From ba58e3bd5ed360d0bd267d7418f64dec40a13e36 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 23 Jul 2019 10:45:40 -0700 Subject: [PATCH 001/662] refactor!: getOptions() no longer accepts GoogleAuthOptions (#749) --- README.md | 8 ++++--- samples/adc.js | 5 ++-- samples/credentials.js | 5 ++-- samples/headers.js | 11 +++++---- samples/keepalive.js | 5 ++-- samples/keyfile.js | 5 ++-- src/auth/googleauth.ts | 52 ++++++++++++++++++++++++++++------------- test/test.googleauth.ts | 42 +++++++++++++++++++++------------ 8 files changed, 86 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index b53e301e..e42b17d5 100644 --- a/README.md +++ b/README.md @@ -46,21 +46,22 @@ Before making your API call, you must be sure the API you're calling has been en Rather than manually creating an OAuth2 client, JWT client, or Compute client, the auth library can create the correct credential type for you, depending upon the environment your code is running under. -For example, a JWT auth client will be created when your code is running on your local developer machine, and a Compute client will be created when the same code is running on Google Cloud Platform. If you need a specific set of scopes, you can pass those in the form of a string or an array into the `auth.getClient` method. +For example, a JWT auth client will be created when your code is running on your local developer machine, and a Compute client will be created when the same code is running on Google Cloud Platform. If you need a specific set of scopes, you can pass those in the form of a string or an array to the `GoogleAuth` constructor. The code below shows how to retrieve a default credential type, depending upon the runtime environment. ```js -const {auth} = require('google-auth-library'); +const {GoogleAuth} = require('google-auth-library'); /** * Instead of specifying the type of client you'd like to use (JWT, OAuth2, etc) * this library will automatically choose the right client based on the environment. */ async function main() { - const client = await auth.getClient({ + const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform' }); + const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({ url }); @@ -168,6 +169,7 @@ main().catch(console.error); ``` #### Handling token events + This library will automatically obtain an `access_token`, and automatically refresh the `access_token` if a `refresh_token` is present. The `refresh_token` is only returned on the [first authorization](https://github.com/googleapis/google-api-nodejs-client/issues/750#issuecomment-304521450), so if you want to make sure you store it safely. An easy way to make sure you always store the most recent tokens is to use the `tokens` event: ```js diff --git a/samples/adc.js b/samples/adc.js index 5168e6a6..c74f8904 100644 --- a/samples/adc.js +++ b/samples/adc.js @@ -16,15 +16,16 @@ /** * Import the GoogleAuth library, and create a new GoogleAuth client. */ -const {auth} = require('google-auth-library'); +const {GoogleAuth} = require('google-auth-library'); /** * Acquire a client, and make a request to an API that's enabled by default. */ async function main() { - const client = await auth.getClient({ + const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform', }); + const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({url}); diff --git a/samples/credentials.js b/samples/credentials.js index 3080b107..9cac46e7 100644 --- a/samples/credentials.js +++ b/samples/credentials.js @@ -16,7 +16,7 @@ /** * Import the GoogleAuth library, and create a new GoogleAuth client. */ -const {auth} = require('google-auth-library'); +const {GoogleAuth} = require('google-auth-library'); /** * This sample demonstrates passing a `credentials` object directly into the @@ -33,13 +33,14 @@ async function main() { this sample. `); } - const client = await auth.getClient({ + const auth = new GoogleAuth({ credentials: { client_email: clientEmail, private_key: privateKey, }, scopes: 'https://www.googleapis.com/auth/cloud-platform', }); + const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({url}); diff --git a/samples/headers.js b/samples/headers.js index 02fe30b1..384b5580 100644 --- a/samples/headers.js +++ b/samples/headers.js @@ -16,7 +16,7 @@ /** * Import the GoogleAuth library, and create a new GoogleAuth client. */ -const {auth} = require('google-auth-library'); +const {GoogleAuth} = require('google-auth-library'); const fetch = require('node-fetch'); /** @@ -25,14 +25,15 @@ const fetch = require('node-fetch'); * node-fetch, but you could use any HTTP client you like. */ async function main() { + // create auth instance with custom scopes. + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }); const projectId = await auth.getProjectId(); const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; // obtain an authenticated client - const client = await auth.getClient({ - scopes: 'https://www.googleapis.com/auth/cloud-platform', - }); - + const client = await auth.getClient(); // Use the client to get authenticated request headers const headers = await client.getRequestHeaders(); console.log('Headers:'); diff --git a/samples/keepalive.js b/samples/keepalive.js index da47c585..34c32348 100644 --- a/samples/keepalive.js +++ b/samples/keepalive.js @@ -23,16 +23,17 @@ /** * Import the GoogleAuth library, and create a new GoogleAuth client. */ -const {auth} = require('google-auth-library'); +const {GoogleAuth} = require('google-auth-library'); const https = require('https'); /** * Acquire a client, and make a request to an API that's enabled by default. */ async function main() { - const client = await auth.getClient({ + const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform', }); + const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; diff --git a/samples/keyfile.js b/samples/keyfile.js index 846416ac..30d143af 100644 --- a/samples/keyfile.js +++ b/samples/keyfile.js @@ -16,7 +16,7 @@ /** * Import the GoogleAuth library, and create a new GoogleAuth client. */ -const {auth} = require('google-auth-library'); +const {GoogleAuth} = require('google-auth-library'); /** * Acquire a client, and make a request to an API that's enabled by default. @@ -25,10 +25,11 @@ async function main( // Full path to the sevice account credential keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS ) { - const client = await auth.getClient({ + const auth = new GoogleAuth({ keyFile: keyFile, scopes: 'https://www.googleapis.com/auth/cloud-platform', }); + const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({url}); diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index e65b440c..3ca8b9f4 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -46,6 +46,8 @@ export interface CredentialCallback { (err: Error | null, result?: UserRefreshClient | JWT): void; } +interface DeprecatedGetClientOptions {} + export interface ADCCallback { ( err: Error | null, @@ -441,7 +443,6 @@ export class GoogleAuth { 'Must pass in a JSON object containing the Google auth settings.' ); } - this.jsonContent = json; options = options || {}; if (json.type === 'authorized_user') { client = new UserRefreshClient(options); @@ -453,6 +454,33 @@ export class GoogleAuth { 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 + */ + private _cacheClientFromJSON( + json: JWTInput, + options?: RefreshOptions + ): JWT | UserRefreshClient { + let client: UserRefreshClient | JWT; + // create either a UserRefreshClient or JWT client. + options = options || {}; + if (json.type === 'authorized_user') { + client = new UserRefreshClient(options); + } else { + (options as JWTOptions).scopes = this.scopes; + client = new JWT(options); + } + client.fromJSON(json); + // cache both raw data used to instantiate client and client itself. + this.jsonContent = json; + this.cachedCredential = client; + return this.cachedCredential; + } + /** * Create a credentials instance using the given input stream. * @param inputStream The input stream. @@ -508,7 +536,7 @@ export class GoogleAuth { .on('end', () => { try { const data = JSON.parse(s); - const r = this.fromJSON(data, options); + const r = this._cacheClientFromJSON(data, options); return resolve(r); } catch (err) { return reject(err); @@ -682,27 +710,19 @@ export class GoogleAuth { * Automatically obtain a client based on the provided configuration. If no * options were passed, use Application Default Credentials. */ - async getClient(options?: GoogleAuthOptions) { + async getClient(options?: DeprecatedGetClientOptions) { if (options) { - this.keyFilename = - options.keyFilename || options.keyFile || this.keyFilename; - this.scopes = options.scopes || this.scopes; - this.jsonContent = options.credentials || this.jsonContent; - this.clientOptions = options.clientOptions; + 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.cachedCredential = await this.fromJSON( - this.jsonContent, - this.clientOptions - ); + this._cacheClientFromJSON(this.jsonContent, this.clientOptions); } else if (this.keyFilename) { const filePath = path.resolve(this.keyFilename); const stream = fs.createReadStream(filePath); - this.cachedCredential = await this.fromStreamAsync( - stream, - this.clientOptions - ); + await this.fromStreamAsync(stream, this.clientOptions); } else { await this.getApplicationDefaultAsync(this.clientOptions); } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 7921af53..1334be6b 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -231,6 +231,16 @@ describe('googleauth', () => { }); }); + it('fromJson should not overwrite previous client configuration', async () => { + const auth = new GoogleAuth({keyFilename: './test/fixtures/private.json'}); + auth.fromJSON({ + client_email: 'batman@example.com', + private_key: 'abc123', + }); + const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.email, 'hello@youarecool.com'); + }); + it('fromAPIKey should error given an invalid api key', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. @@ -1124,7 +1134,7 @@ describe('googleauth', () => { it('should use jsonContent if available', async () => { const json = createJwtJSON(); - auth.fromJSON(json); + const auth = new GoogleAuth({credentials: json}); // We know this returned a cached result if a nock scope isn't required const body = await auth.getCredentials(); assert.notStrictEqual(body, null); @@ -1138,10 +1148,8 @@ describe('googleauth', () => { }); it('should error when invalid keyFilename passed to getClient', async () => { - await assertRejects( - auth.getClient({keyFilename: './funky/fresh.json'}), - /ENOENT: no such file or directory/ - ); + const auth = new GoogleAuth({keyFilename: './funky/fresh.json'}); + await assertRejects(auth.getClient(), /ENOENT: no such file or directory/); }); it('should accept credentials to get a client', async () => { @@ -1165,21 +1173,24 @@ describe('googleauth', () => { it('should allow passing scopes to get a client', async () => { const scopes = ['http://examples.com/is/a/scope']; const keyFilename = './test/fixtures/private.json'; - const client = (await auth.getClient({scopes, keyFilename})) as JWT; + const auth = new GoogleAuth({scopes, keyFilename}); + const client = (await auth.getClient()) as JWT; assert.strictEqual(client.scopes, scopes); }); it('should allow passing a scope to get a client', async () => { const scopes = 'http://examples.com/is/a/scope'; const keyFilename = './test/fixtures/private.json'; - const client = (await auth.getClient({scopes, keyFilename})) as JWT; + const auth = new GoogleAuth({scopes, keyFilename}); + const client = (await auth.getClient()) as JWT; assert.strictEqual(client.scopes, scopes); }); it('should allow passing a scope to get a Compute client', async () => { const scopes = ['http://examples.com/is/a/scope']; const nockScopes = [nockIsGCE(), createGetProjectIdNock()]; - const client = (await auth.getClient({scopes})) as Compute; + const auth = new GoogleAuth({scopes}); + const client = (await auth.getClient()) as Compute; assert.strictEqual(client.scopes, scopes); nockScopes.forEach(x => x.done()); }); @@ -1348,13 +1359,6 @@ describe('googleauth', () => { assert.strictEqual(count, 0); }); - it('should pass options to the JWT constructor via getClient', async () => { - const subject = 'science!'; - const auth = new GoogleAuth({keyFilename: './test/fixtures/private.json'}); - const client = (await auth.getClient({clientOptions: {subject}})) as JWT; - assert.strictEqual(client.subject, subject); - }); - it('should pass options to the JWT constructor via constructor', async () => { const subject = 'science!'; const auth = new GoogleAuth({ @@ -1373,4 +1377,12 @@ describe('googleauth', () => { /Unable to detect a Project Id in the current environment/ ); }); + + it('should throw if options are passed to getClient()', async () => { + const auth = new GoogleAuth(); + await assertRejects( + auth.getClient({hello: 'world'}), + /Passing options to getClient is forbidden in v5.0.0/ + ); + }); }); From 944e2aa62a61c253ba153f49590d7416585c64eb Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 23 Jul 2019 11:06:16 -0700 Subject: [PATCH 002/662] chore: release 5.0.0 (#752) --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5dd4525..acb6515d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.0.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v4.2.6...v5.0.0) (2019-07-23) + + +### ⚠ BREAKING CHANGES + +* getOptions() no longer accepts GoogleAuthOptions (#749) + +### Code Refactoring + +* getOptions() no longer accepts GoogleAuthOptions ([#749](https://www.github.com/googleapis/google-auth-library-nodejs/issues/749)) ([ba58e3b](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ba58e3b)) + ### [4.2.6](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v4.2.5...v4.2.6) (2019-07-23) diff --git a/package.json b/package.json index 42d50ae1..6ad67046 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "4.2.6", + "version": "5.0.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 9a9fa366..fa80c3d7 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^4.2.6", + "google-auth-library": "^5.0.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 5577f0d8ced78b776ca93ba3caf15d7f3da60096 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 24 Jul 2019 11:00:12 -0700 Subject: [PATCH 003/662] feat(types): expose ProjectIdCallback interface (#753) --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 6d603a20..78401965 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ export { JWTInput, } from './auth/credentials'; export {GCPEnv} from './auth/envDetect'; -export {GoogleAuthOptions} from './auth/googleauth'; +export {GoogleAuthOptions, ProjectIdCallback} from './auth/googleauth'; export {IAMAuth, RequestMetadata} from './auth/iam'; export {Claims, JWTAccess} from './auth/jwtaccess'; export {JWT, JWTOptions} from './auth/jwtclient'; From 7013fe8a423351d40a2a9b1ae2bd58bc9041a2b5 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 24 Jul 2019 11:11:25 -0700 Subject: [PATCH 004/662] chore: release 5.1.0 (#754) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acb6515d..16571ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.1.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.0.0...v5.1.0) (2019-07-24) + + +### Features + +* **types:** expose ProjectIdCallback interface ([#753](https://www.github.com/googleapis/google-auth-library-nodejs/issues/753)) ([5577f0d](https://www.github.com/googleapis/google-auth-library-nodejs/commit/5577f0d)) + ## [5.0.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v4.2.6...v5.0.0) (2019-07-23) diff --git a/package.json b/package.json index 6ad67046..560537fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.0.0", + "version": "5.1.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index fa80c3d7..f39716d1 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.0.0", + "google-auth-library": "^5.1.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From e32a12bb8c9f326e2bfad78f367fce04fc185bda Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 26 Jul 2019 18:17:49 +0300 Subject: [PATCH 005/662] fix(deps): update dependency google-auth-library to v5 (#759) --- samples/puppeteer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index ebc4bbc0..4ed0e71e 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -11,7 +11,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^4.0.0", + "google-auth-library": "^5.0.0", "puppeteer": "^1.0.0" } } From c1f1bf7374ba2aca32467e516ba232f07c42180c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 26 Jul 2019 18:58:22 +0300 Subject: [PATCH 006/662] chore(deps): update linters (#758) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 560537fd..7cba7ac6 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ "assert-rejects": "^1.0.0", "chai": "^4.2.0", "codecov": "^3.0.2", - "eslint": "^5.10.0", - "eslint-config-prettier": "^4.0.0", + "eslint": "^6.0.0", + "eslint-config-prettier": "^6.0.0", "eslint-plugin-node": "^9.0.0", "eslint-plugin-prettier": "^3.0.0", "execa": "^1.0.0", From 0ac5285d98d6590f1777db5e1d25ae3e4c2b5867 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 26 Jul 2019 20:51:52 +0300 Subject: [PATCH 007/662] chore(deps): update dependency karma-chrome-launcher to v3 (#757) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7cba7ac6..56560406 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "is-docker": "^2.0.0", "js-green-licenses": "^1.0.0", "karma": "^4.0.0", - "karma-chrome-launcher": "^2.2.0", + "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^1.1.2", "karma-firefox-launcher": "^1.1.0", "karma-mocha": "^1.3.0", From 5f9de4cd61b59dcd1d7fc80db5c618bb6d4aa47b Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 26 Jul 2019 10:52:11 -0700 Subject: [PATCH 008/662] chore(deps): update dependency execa to v2 (#762) --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 56560406..f8cf9ed9 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "@compodoc/compodoc": "^1.1.7", "@types/base64-js": "^1.2.5", "@types/chai": "^4.1.7", - "@types/execa": "^0.9.0", "@types/jws": "^3.1.0", "@types/lru-cache": "^5.0.0", "@types/mocha": "^5.2.1", @@ -47,7 +46,7 @@ "eslint-config-prettier": "^6.0.0", "eslint-plugin-node": "^9.0.0", "eslint-plugin-prettier": "^3.0.0", - "execa": "^1.0.0", + "execa": "^2.0.0", "gts": "^1.0.0", "is-docker": "^2.0.0", "js-green-licenses": "^1.0.0", From baae0d2499c46ee7df445e7dfcd274dc05c91054 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 29 Jul 2019 08:33:49 -0700 Subject: [PATCH 009/662] chore: release 5.1.1 (#761) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json [ci skip] --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16571ccd..a6d85982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.1.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.1.0...v5.1.1) (2019-07-29) + + +### Bug Fixes + +* **deps:** update dependency google-auth-library to v5 ([#759](https://www.github.com/googleapis/google-auth-library-nodejs/issues/759)) ([e32a12b](https://www.github.com/googleapis/google-auth-library-nodejs/commit/e32a12b)) + ## [5.1.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.0.0...v5.1.0) (2019-07-24) diff --git a/package.json b/package.json index f8cf9ed9..7a6fb9f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.1.0", + "version": "5.1.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index f39716d1..0ce35ff8 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.1.0", + "google-auth-library": "^5.1.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From a1fcc25924dd716f7de4ed5495447cf7e3d3cb8d Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 29 Jul 2019 12:56:18 -0700 Subject: [PATCH 010/662] fix(deps): upgrade to gtoken 4.x (#763) --- package.json | 2 +- src/auth/jwtclient.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7a6fb9f7..e8ea64a1 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "fast-text-encoding": "^1.0.0", "gaxios": "^2.0.0", "gcp-metadata": "^2.0.0", - "gtoken": "^3.0.0", + "gtoken": "^4.0.0", "jws": "^3.1.5", "lru-cache": "^5.0.0" }, diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 92554879..54074cc5 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -211,11 +211,10 @@ export class JWT extends OAuth2Client { const gtoken = this.createGToken(); const token = await gtoken.getToken(); const tokens = { - access_token: token, + access_token: token.access_token, token_type: 'Bearer', expiry_date: gtoken.expiresAt, - // tslint:disable-next-line no-any - id_token: (gtoken.rawToken! as any).id_token, + id_token: gtoken.idToken, }; this.emit('tokens', tokens); return {res: null, tokens}; From 5f98dd9e2b68182c6f28f223afa91666a0bf8054 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Wed, 31 Jul 2019 08:33:00 -0700 Subject: [PATCH 011/662] docs: use the jsdoc-fresh theme (#767) --- .jsdoc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jsdoc.js b/.jsdoc.js index 6d9efcbe..4c8c69ee 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -20,7 +20,7 @@ module.exports = { opts: { readme: './README.md', package: './package.json', - template: './node_modules/jsdoc-baseline', + template: './node_modules/jsdoc-fresh', recurse: true, verbose: true, destination: './docs/' From 1ba3668742f3a28d767bdfd69c33c9ede73afb17 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 31 Jul 2019 17:11:47 -0700 Subject: [PATCH 012/662] build: add Node 12 remove Node 11 (#768) --- .kokoro/continuous/{node11 => node12}/common.cfg | 2 +- .kokoro/continuous/{node11 => node12}/test.cfg | 0 .kokoro/presubmit/{node11 => node12}/common.cfg | 2 +- .kokoro/presubmit/{node11 => node12}/test.cfg | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename .kokoro/continuous/{node11 => node12}/common.cfg (90%) rename .kokoro/continuous/{node11 => node12}/test.cfg (100%) rename .kokoro/presubmit/{node11 => node12}/common.cfg (90%) rename .kokoro/presubmit/{node11 => node12}/test.cfg (100%) diff --git a/.kokoro/continuous/node11/common.cfg b/.kokoro/continuous/node12/common.cfg similarity index 90% rename from .kokoro/continuous/node11/common.cfg rename to .kokoro/continuous/node12/common.cfg index 8f5703df..f2825615 100644 --- a/.kokoro/continuous/node11/common.cfg +++ b/.kokoro/continuous/node12/common.cfg @@ -16,7 +16,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:11-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/continuous/node11/test.cfg b/.kokoro/continuous/node12/test.cfg similarity index 100% rename from .kokoro/continuous/node11/test.cfg rename to .kokoro/continuous/node12/test.cfg diff --git a/.kokoro/presubmit/node11/common.cfg b/.kokoro/presubmit/node12/common.cfg similarity index 90% rename from .kokoro/presubmit/node11/common.cfg rename to .kokoro/presubmit/node12/common.cfg index 8f5703df..f2825615 100644 --- a/.kokoro/presubmit/node11/common.cfg +++ b/.kokoro/presubmit/node12/common.cfg @@ -16,7 +16,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:11-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/presubmit/node11/test.cfg b/.kokoro/presubmit/node12/test.cfg similarity index 100% rename from .kokoro/presubmit/node11/test.cfg rename to .kokoro/presubmit/node12/test.cfg From f7a40464f404d66068faa5b466d52355afb298a5 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Wed, 31 Jul 2019 23:55:48 -0700 Subject: [PATCH 013/662] chore(deps): use the beta release of nock (#769) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8ea64a1..f2fd7a23 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "mocha": "^6.0.0", "mv": "^2.1.1", "ncp": "^2.0.0", - "nock": "^10.0.0", + "nock": "^11.0.0-beta.30", "null-loader": "^3.0.0", "nyc": "^14.1.1", "prettier": "^1.13.4", From ed7384a0e930a3cbfc1733042faac07a11d7641e Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 5 Aug 2019 10:03:26 -0700 Subject: [PATCH 014/662] chore: release 5.1.2 (#765) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json [ci skip] --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d85982..a1e289ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.1.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.1.1...v5.1.2) (2019-08-05) + + +### Bug Fixes + +* **deps:** upgrade to gtoken 4.x ([#763](https://www.github.com/googleapis/google-auth-library-nodejs/issues/763)) ([a1fcc25](https://www.github.com/googleapis/google-auth-library-nodejs/commit/a1fcc25)) + ### [5.1.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.1.0...v5.1.1) (2019-07-29) diff --git a/package.json b/package.json index f2fd7a23..ec889a82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.1.1", + "version": "5.1.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 0ce35ff8..74962ec3 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.1.1", + "google-auth-library": "^5.1.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 526dcf6a8ab36fc8e8f6518ccc021c1ecbab025e Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 9 Aug 2019 13:12:31 -0700 Subject: [PATCH 015/662] feat: populate x-goog-api-client header for auth (#772) --- src/transporters.ts | 12 ++++++++++++ test/test.transporters.ts | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/transporters.ts b/src/transporters.ts index c49ead28..2a463a3f 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -68,6 +68,18 @@ export class DefaultTransporter { 'User-Agent' ] = `${uaValue} ${DefaultTransporter.USER_AGENT}`; } + // track google-auth-library-nodejs version: + const authVersion = `auth/${pkg.version}`; + if (opts.headers['x-goog-api-client']) { + opts.headers[ + 'x-goog-api-client' + ] = `${opts.headers['x-goog-api-client']} ${authVersion}`; + } else { + const nodeVersion = process.version.replace(/^v/, ''); + opts.headers[ + 'x-goog-api-client' + ] = `gl-node/${nodeVersion} ${authVersion}`; + } } return opts; } diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 8d75ea6a..c4f2b67f 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -55,6 +55,28 @@ it('should not append default client user agent to the existing user agent more assert.strictEqual(opts.headers!['User-Agent'], appName); }); +it('should add x-goog-api-client header if none exists', () => { + const opts = transporter.configure({ + url: '', + }); + assert( + /^gl-node\/[.-\w$]+ auth\/[.-\w$]+$/.test( + opts.headers!['x-goog-api-client'] + ) + ); +}); + +it('should append to x-goog-api-client header if it exists', () => { + const opts = transporter.configure({ + headers: {'x-goog-api-client': 'gdcl/1.0.0'}, + url: '', + }); + console.info(opts.headers); + assert( + /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client']) + ); +}); + it('should create a single error from multiple response errors', done => { const firstError = {message: 'Error 1'}; const secondError = {message: 'Error 2'}; From e9858fc3adba737a33b1d4e3c9e46c62f56dc30c Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 9 Aug 2019 14:27:31 -0700 Subject: [PATCH 016/662] chore: release 5.2.0 (#773) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e289ee..5970455a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.2.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.1.2...v5.2.0) (2019-08-09) + + +### Features + +* populate x-goog-api-client header for auth ([#772](https://www.github.com/googleapis/google-auth-library-nodejs/issues/772)) ([526dcf6](https://www.github.com/googleapis/google-auth-library-nodejs/commit/526dcf6)) + ### [5.1.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.1.1...v5.1.2) (2019-08-05) diff --git a/package.json b/package.json index ec889a82..cb029475 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.1.2", + "version": "5.2.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 74962ec3..2d9f40e7 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.1.2", + "google-auth-library": "^5.2.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 87fee68c607337c5cc7e4a34a8b34daca88d3aab Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 24 Aug 2019 05:45:30 +0300 Subject: [PATCH 017/662] chore(deps): update dependency karma-coverage to v2 (#776) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb029475..857add95 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "js-green-licenses": "^1.0.0", "karma": "^4.0.0", "karma-chrome-launcher": "^3.0.0", - "karma-coverage": "^1.1.2", + "karma-coverage": "^2.0.0", "karma-firefox-launcher": "^1.1.0", "karma-mocha": "^1.3.0", "karma-remap-coverage": "^0.1.5", From 4b949ddacdbaa4bef135bc5cbab1db437990c050 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 29 Aug 2019 18:36:05 +0300 Subject: [PATCH 018/662] chore(deps): update dependency typescript to ~3.6.0 (#778) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 857add95..a888cb31 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "source-map-support": "^0.5.6", "tmp": "^0.1.0", "ts-loader": "^6.0.0", - "typescript": "~3.5.0", + "typescript": "~3.6.0", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" }, From feeee502474b03e405faac5fb520d9b8a9b638eb Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Tue, 3 Sep 2019 15:38:52 -0700 Subject: [PATCH 019/662] docs(samples): update dns host name in samples (#780) --- README.md | 10 +++++----- samples/adc.js | 2 +- samples/compute.js | 2 +- samples/credentials.js | 2 +- samples/headers.js | 2 +- samples/jwt.js | 2 +- samples/keepalive.js | 2 +- samples/keyfile.js | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e42b17d5..7b912223 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ async function main() { }); const client = await auth.getClient(); const projectId = await auth.getProjectId(); - const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({ url }); console.log(res.data); } @@ -183,7 +183,7 @@ client.on('tokens', (tokens) => { console.log(tokens.access_token); }); -const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; +const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({ url }); // The `tokens` event would now be raised if this was the first request ``` @@ -256,7 +256,7 @@ async function main() { keys.private_key, ['https://www.googleapis.com/auth/cloud-platform'], ); - const url = `https://www.googleapis.com/dns/v1/projects/${keys.project_id}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; const res = await client.request({url}); console.log(res.data); } @@ -301,7 +301,7 @@ async function main() { // load the JWT or UserRefreshClient from the keys const client = auth.fromJSON(keys); client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; - const url = `https://www.googleapis.com/dns/v1/projects/${keys.project_id}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; const res = await client.request({url}); console.log(res.data); } @@ -326,7 +326,7 @@ async function main() { serviceAccountEmail: 'my-service-account@example.com' }); const projectId = await auth.getProjectId(); - const url = `https://www.googleapis.com/dns/v1/projects/${project_id}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${project_id}`; const res = await client.request({url}); console.log(res.data); } diff --git a/samples/adc.js b/samples/adc.js index c74f8904..2e0fc01c 100644 --- a/samples/adc.js +++ b/samples/adc.js @@ -27,7 +27,7 @@ async function main() { }); const client = await auth.getClient(); const projectId = await auth.getProjectId(); - const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({url}); console.log('DNS Info:'); console.log(res.data); diff --git a/samples/compute.js b/samples/compute.js index 1c900f18..b4f22596 100644 --- a/samples/compute.js +++ b/samples/compute.js @@ -27,7 +27,7 @@ async function main() { serviceAccountEmail: 'some-service-account@example.com', }); const projectId = await auth.getProjectId(); - const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({url}); console.log(res.data); } diff --git a/samples/credentials.js b/samples/credentials.js index 9cac46e7..acbefac6 100644 --- a/samples/credentials.js +++ b/samples/credentials.js @@ -42,7 +42,7 @@ async function main() { }); const client = await auth.getClient(); const projectId = await auth.getProjectId(); - const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({url}); console.log('DNS Info:'); console.log(res.data); diff --git a/samples/headers.js b/samples/headers.js index 384b5580..4301b2b7 100644 --- a/samples/headers.js +++ b/samples/headers.js @@ -30,7 +30,7 @@ async function main() { scopes: 'https://www.googleapis.com/auth/cloud-platform', }); const projectId = await auth.getProjectId(); - const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; // obtain an authenticated client const client = await auth.getClient(); diff --git a/samples/jwt.js b/samples/jwt.js index 72bb83f2..e5530e4e 100644 --- a/samples/jwt.js +++ b/samples/jwt.js @@ -35,7 +35,7 @@ async function main( key: keys.private_key, scopes: ['https://www.googleapis.com/auth/cloud-platform'], }); - const url = `https://www.googleapis.com/dns/v1/projects/${keys.project_id}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; const res = await client.request({url}); console.log('DNS Info:'); console.log(res.data); diff --git a/samples/keepalive.js b/samples/keepalive.js index 34c32348..9102be2f 100644 --- a/samples/keepalive.js +++ b/samples/keepalive.js @@ -35,7 +35,7 @@ async function main() { }); const client = await auth.getClient(); const projectId = await auth.getProjectId(); - const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; // create a new agent with keepAlive enabled const agent = new https.Agent({keepAlive: true}); diff --git a/samples/keyfile.js b/samples/keyfile.js index 30d143af..5915f518 100644 --- a/samples/keyfile.js +++ b/samples/keyfile.js @@ -31,7 +31,7 @@ async function main( }); const client = await auth.getClient(); const projectId = await auth.getProjectId(); - const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({url}); console.log('DNS Info:'); console.log(res.data); From d8c70b9d858e1ef07cb8ef2b5d5d560ac2b2600a Mon Sep 17 00:00:00 2001 From: Jonas Laux Date: Thu, 5 Sep 2019 20:15:58 +0200 Subject: [PATCH 020/662] fix(docs): fix variable name in README.md (#782) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b912223..65cbaa7b 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ async function main() { serviceAccountEmail: 'my-service-account@example.com' }); const projectId = await auth.getProjectId(); - const url = `https://dns.googleapis.com/dns/v1/projects/${project_id}`; + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; const res = await client.request({url}); console.log(res.data); } From a253709a1250661fb2f6541b4dd640a114a57870 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Thu, 5 Sep 2019 14:44:35 -0400 Subject: [PATCH 021/662] fix(deps): nock@next has types that work with our libraries (#783) --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index a888cb31..7bb6cd00 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "@types/mocha": "^5.2.1", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", - "@types/nock": "^10.0.3", "@types/node": "^10.5.1", "@types/sinon": "^7.0.0", "@types/tmp": "^0.1.0", @@ -63,7 +62,7 @@ "mocha": "^6.0.0", "mv": "^2.1.1", "ncp": "^2.0.0", - "nock": "^11.0.0-beta.30", + "nock": "^11.3.2", "null-loader": "^3.0.0", "nyc": "^14.1.1", "prettier": "^1.13.4", From 6d3524d23175b553e2a7525a0ffffb0369ea1f4d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 5 Sep 2019 21:57:41 +0300 Subject: [PATCH 022/662] chore(deps): update dependency eslint-plugin-node to v10 (#781) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7bb6cd00..f260d9c4 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "codecov": "^3.0.2", "eslint": "^6.0.0", "eslint-config-prettier": "^6.0.0", - "eslint-plugin-node": "^9.0.0", + "eslint-plugin-node": "^10.0.0", "eslint-plugin-prettier": "^3.0.0", "execa": "^2.0.0", "gts": "^1.0.0", From 53a14cec748a841e55a3447cecc858040c5eb497 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 6 Sep 2019 08:51:44 -0700 Subject: [PATCH 023/662] chore: release 5.2.1 (#784) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json [ci skip] --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5970455a..ce0885cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.2.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.2.0...v5.2.1) (2019-09-06) + + +### Bug Fixes + +* **deps:** nock@next has types that work with our libraries ([#783](https://www.github.com/googleapis/google-auth-library-nodejs/issues/783)) ([a253709](https://www.github.com/googleapis/google-auth-library-nodejs/commit/a253709)) +* **docs:** fix variable name in README.md ([#782](https://www.github.com/googleapis/google-auth-library-nodejs/issues/782)) ([d8c70b9](https://www.github.com/googleapis/google-auth-library-nodejs/commit/d8c70b9)) + ## [5.2.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.1.2...v5.2.0) (2019-08-09) diff --git a/package.json b/package.json index f260d9c4..be816243 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.2.0", + "version": "5.2.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 2d9f40e7..58b24ad9 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.2.0", + "google-auth-library": "^5.2.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From e1d671898a09835ed16afbd25cd9e142237a6045 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 6 Sep 2019 18:11:36 -0400 Subject: [PATCH 024/662] update .nycrc ignore rules (#785) --- .nycrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.nycrc b/.nycrc index 83a421a0..23e32220 100644 --- a/.nycrc +++ b/.nycrc @@ -6,6 +6,7 @@ "**/.coverage", "**/apis", "**/benchmark", + "**/conformance", "**/docs", "**/samples", "**/scripts", From 651b5d43ee67e8d4d531929c28afbe795ed3b697 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 16 Sep 2019 20:26:03 -0700 Subject: [PATCH 025/662] fix(deps): update to gcp-metadata and address envDetect performance issues (#787) * fix(deps): update to gcp-metadata that does not throw locally * chore: fix up tests * chore: fix linting --- package.json | 2 +- src/auth/envDetect.ts | 8 +++++--- test/test.googleauth.ts | 44 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index be816243..1b085a84 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "base64-js": "^1.3.0", "fast-text-encoding": "^1.0.0", "gaxios": "^2.0.0", - "gcp-metadata": "^2.0.0", + "gcp-metadata": "^3.0.0", "gtoken": "^4.0.0", "jws": "^3.1.5", "lru-cache": "^5.0.0" diff --git a/src/auth/envDetect.ts b/src/auth/envDetect.ts index 39bdfe6f..e3b15374 100644 --- a/src/auth/envDetect.ts +++ b/src/auth/envDetect.ts @@ -36,10 +36,12 @@ export async function getEnv() { env = GCPEnv.APP_ENGINE; } else if (isCloudFunction()) { env = GCPEnv.CLOUD_FUNCTIONS; - } else if (await isKubernetesEngine()) { - env = GCPEnv.KUBERNETES_ENGINE; } else if (await isComputeEngine()) { - env = GCPEnv.COMPUTE_ENGINE; + if (await isKubernetesEngine()) { + env = GCPEnv.KUBERNETES_ENGINE; + } else { + env = GCPEnv.COMPUTE_ENGINE; + } } else { env = GCPEnv.NONE; } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 1334be6b..d730394a 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -19,7 +19,12 @@ const assertRejects = require('assert-rejects'); import * as child_process from 'child_process'; import * as crypto from 'crypto'; import * as fs from 'fs'; -import {BASE_PATH, HEADERS, HOST_ADDRESS} from 'gcp-metadata'; +import { + BASE_PATH, + HEADERS, + HOST_ADDRESS, + SECONDARY_HOST_ADDRESS, +} from 'gcp-metadata'; import * as nock from 'nock'; import * as os from 'os'; import * as path from 'path'; @@ -158,21 +163,50 @@ describe('googleauth', () => { } function nockIsGCE() { - return nock(host) + const primary = nock(host) + .get(instancePath) + .reply(200, {}, HEADERS); + const secondary = nock(SECONDARY_HOST_ADDRESS) .get(instancePath) .reply(200, {}, HEADERS); + + return { + done: () => { + primary.done(); + secondary.done(); + }, + }; } function nockNotGCE() { - return nock(host) + const primary = nock(host) + .get(instancePath) + .replyWithError({code: 'ENOTFOUND'}); + const secondary = nock(SECONDARY_HOST_ADDRESS) .get(instancePath) .replyWithError({code: 'ENOTFOUND'}); + return { + done: () => { + primary.done(); + secondary.done(); + }, + }; } function nock500GCE() { - return nock(host) + const primary = nock(host) + .get(instancePath) + .reply(500, {}, HEADERS); + const secondary = nock(SECONDARY_HOST_ADDRESS) .get(instancePath) - .reply(500); + .reply(500, {}, HEADERS); + + return { + done: () => { + primary.done(); + secondary.done(); + }, + }; } function nock404GCE() { From 52456d8f868b26f43a08c238f733c9cd92a151c0 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2019 10:36:42 -0400 Subject: [PATCH 026/662] chore: release 5.2.2 (#788) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json [ci skip] --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0885cb..6cf523c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.2.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.2.1...v5.2.2) (2019-09-17) + + +### Bug Fixes + +* **deps:** update to gcp-metadata and address envDetect performance issues ([#787](https://www.github.com/googleapis/google-auth-library-nodejs/issues/787)) ([651b5d4](https://www.github.com/googleapis/google-auth-library-nodejs/commit/651b5d4)) + ### [5.2.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.2.0...v5.2.1) (2019-09-06) diff --git a/package.json b/package.json index 1b085a84..85c08f5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.2.1", + "version": "5.2.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { From 9313658b64b9a1600b8b51be2585316a3325cbd6 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 18 Sep 2019 09:44:39 -0700 Subject: [PATCH 027/662] build: switch to repo-automation-bots for releases --- .github/release-please.yml | 0 .kokoro/continuous/node10/test.cfg | 28 ---------------------------- .kokoro/test.sh | 9 --------- synth.metadata | 2 +- 4 files changed, 1 insertion(+), 38 deletions(-) create mode 100644 .github/release-please.yml diff --git a/.github/release-please.yml b/.github/release-please.yml new file mode 100644 index 00000000..e69de29b diff --git a/.kokoro/continuous/node10/test.cfg b/.kokoro/continuous/node10/test.cfg index fefee48b..468b8c71 100644 --- a/.kokoro/continuous/node10/test.cfg +++ b/.kokoro/continuous/node10/test.cfg @@ -7,31 +7,3 @@ before_action { } } } - -# tokens used by release-please to keep an up-to-date release PR. -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "github-magic-proxy-key-release-please" - } - } -} - -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "github-magic-proxy-token-release-please" - } - } -} - -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "github-magic-proxy-url-release-please" - } - } -} diff --git a/.kokoro/test.sh b/.kokoro/test.sh index 9c0db071..9db11bb0 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -35,12 +35,3 @@ if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then else echo "coverage is only reported for Node $COVERAGE_NODE" fi - -# if release-please keys set, we kick off a task to update the release-PR. -if [ -f ${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please ]; then - npx release-please release-pr --token=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-token-release-please \ - --repo-url=googleapis/google-auth-library-nodejs \ - --package-name=google-auth-library \ - --api-url=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please \ - --proxy-key=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-key-release-please -fi diff --git a/synth.metadata b/synth.metadata index 6a949fee..38a933d0 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,5 +1,5 @@ { - "updateTime": "2019-06-18T00:57:07.705975Z", + "updateTime": "2019-09-18T11:06:57.065713Z", "sources": [ { "template": { From 8640028038ad610407296dc861651c81ceb6f840 Mon Sep 17 00:00:00 2001 From: Alexander Fenster Date: Fri, 27 Sep 2019 02:04:29 -0400 Subject: [PATCH 028/662] chore: add protos/ to .eslintignore --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index f0c7aead..09b31fe7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ src/**/doc/* build/ docs/ +protos/ From fecd4f441f6e5f3024b0ce18fe54f33b21c572a4 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 27 Sep 2019 15:24:43 -0400 Subject: [PATCH 029/662] feat: if token expires soon, force refresh (#794) --- package.json | 2 +- src/auth/jwtclient.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 85c08f5f..c806ac96 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "fast-text-encoding": "^1.0.0", "gaxios": "^2.0.0", "gcp-metadata": "^3.0.0", - "gtoken": "^4.0.0", + "gtoken": "^4.1.0", "jws": "^3.1.5", "lru-cache": "^5.0.0" }, diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 54074cc5..0374b1f5 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -209,7 +209,9 @@ export class JWT extends OAuth2Client { refreshToken?: string | null ): Promise { const gtoken = this.createGToken(); - const token = await gtoken.getToken(); + const token = await gtoken.getToken({ + forceRefresh: this.isTokenExpiring(), + }); const tokens = { access_token: token.access_token, token_type: 'Bearer', From 331764acf4239b5cbd395607c71e9053314be37a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2019 13:24:50 -0700 Subject: [PATCH 030/662] chore: release 5.3.0 (#795) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cf523c4..5c663b26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.3.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.2.2...v5.3.0) (2019-09-27) + + +### Features + +* if token expires soon, force refresh ([#794](https://www.github.com/googleapis/google-auth-library-nodejs/issues/794)) ([fecd4f4](https://www.github.com/googleapis/google-auth-library-nodejs/commit/fecd4f4)) + ### [5.2.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.2.1...v5.2.2) (2019-09-17) diff --git a/package.json b/package.json index c806ac96..f398aa97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.2.2", + "version": "5.3.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { From d7547714af0bb405b8230cee189419efd4a75f0c Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 27 Sep 2019 17:37:04 -0400 Subject: [PATCH 031/662] chore: update pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 80975030..46cd1076 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,7 @@ -Fixes # (it's a good idea to open an issue first for discussion) - -- [ ] Tests and linter pass +Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: +- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/{{metadata['repo']['name']}}/issues) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +- [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) + +Fixes # 🦕 From f05de115c199618fc471ce614baa3c4bb60ec6c2 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 8 Oct 2019 12:54:44 -0700 Subject: [PATCH 032/662] feat: do not deprecate refreshAccessToken (#804) --- CHANGELOG.md | 9 ++++++--- src/auth/oauth2client.ts | 1 - src/messages.ts | 9 --------- test/test.googleauth.ts | 23 ++++++++++++++++++++--- test/test.oauth2.ts | 4 ++-- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c663b26..4f0cf04f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -399,10 +399,13 @@ const client = await auth.getClient({ }); ``` -#### The `refreshAccessToken` method has been deprecated -The `OAuth2.refreshAccessToken` method has been deprecated. The `getAccessToken`, `getRequestMetadata`, and `request` methods will all refresh the token if needed automatically. There is no need to ever manually refresh the token. +#### Deprecate `refreshAccessToken` -As always, if you run into any problems... please let us know! +_Note: `refreshAccessToken` is no longer deprecated._ + +`getAccessToken`, `getRequestMetadata`, and `request` methods will all refresh the token if needed automatically. + +You should not need to invoke `refreshAccessToken` directly except in [certain edge-cases](https://github.com/googleapis/google-auth-library-nodejs/issues/575). ### Features - Set private_key_id in JWT access token header like other google auth libraries. (#450) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 5ec8543c..2b04c2a5 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -640,7 +640,6 @@ export class OAuth2Client extends AuthClient { refreshAccessToken( callback?: RefreshAccessTokenCallback ): Promise | void { - messages.warn(messages.REFRESH_ACCESS_TOKEN_DEPRECATED); if (callback) { this.refreshAccessTokenAsync().then( r => callback(null, r.credentials, r.res), diff --git a/src/messages.ts b/src/messages.ts index cb575912..9d90cc59 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -102,15 +102,6 @@ export const JWT_ACCESS_CREATE_SCOPED_DEPRECATED = { ].join(' '), }; -export const REFRESH_ACCESS_TOKEN_DEPRECATED = { - code: 'google-auth-library:DEP007', - type: WarningTypes.DEPRECATION, - message: [ - 'The `refreshAccessToken` method has been deprecated, and will be removed', - 'in the 3.0 release of google-auth-library. Please use the `getRequestHeaders`', - 'method instead.', - ].join(' '), -}; export const OAUTH_GET_REQUEST_METADATA_DEPRECATED = { code: 'google-auth-library:DEP004', type: WarningTypes.DEPRECATION, diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index d730394a..a8259ce3 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -203,16 +203,33 @@ describe('googleauth', () => { return { done: () => { - primary.done(); - secondary.done(); + try { + primary.done(); + secondary.done(); + } catch (err) { + // secondary can sometimes complete prior to primary. + } }, }; } function nock404GCE() { - return nock(host) + const primary = nock(host) .get(instancePath) .reply(404); + const secondary = nock(SECONDARY_HOST_ADDRESS) + .get(instancePath) + .reply(404); + return { + done: () => { + try { + primary.done(); + secondary.done(); + } catch (err) { + // secondary can sometimes complete prior to primary. + } + }, + }; } function createGetProjectIdNock(projectId = 'not-real') { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 892a2a76..677df9b5 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -865,11 +865,11 @@ describe(__filename, () => { }); }); - it('should emit warning on refreshAccessToken', async () => { + it('should not emit warning on refreshAccessToken', async () => { let warned = false; sandbox.stub(process, 'emitWarning').callsFake(() => (warned = true)); client.refreshAccessToken((err, result) => { - assert.strictEqual(warned, true); + assert.strictEqual(warned, false); }); }); From 08d038af73589aed460c01acd21f8f58fef01f5d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2019 13:28:42 -0700 Subject: [PATCH 033/662] chore: release 5.4.0 (#805) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0cf04f..cbef0ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.4.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.3.0...v5.4.0) (2019-10-08) + + +### Features + +* do not deprecate refreshAccessToken ([#804](https://www.github.com/googleapis/google-auth-library-nodejs/issues/804)) ([f05de11](https://www.github.com/googleapis/google-auth-library-nodejs/commit/f05de11)) + ## [5.3.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.2.2...v5.3.0) (2019-09-27) diff --git a/package.json b/package.json index f398aa97..f1cd8935 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.3.0", + "version": "5.4.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { From 64c274ec6ac9985f100f654cde2352b7c570efb4 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Thu, 10 Oct 2019 10:36:45 -0700 Subject: [PATCH 034/662] chore: update CONTRIBUTING.md and make releaseType node (#806) --- .github/release-please.yml | 1 + CONTRIBUTING.md | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.github/release-please.yml b/.github/release-please.yml index e69de29b..85344b92 100644 --- a/.github/release-please.yml +++ b/.github/release-please.yml @@ -0,0 +1 @@ +releaseType: node diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78aaa61b..f6c4cf01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,7 @@ accept your pull requests. 1. Ensure that your code adheres to the existing style in the code to which you are contributing. 1. Ensure that your code has an appropriate set of tests which all pass. +1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling. 1. Submit a pull request. ## Running the tests @@ -46,8 +47,17 @@ accept your pull requests. 1. Run the tests: + # Run unit tests. npm test + # Run sample integration tests. + gcloud auth application-default login + npm run samples-test + + # Run all system tests. + gcloud auth application-default login + npm run system-test + 1. Lint (and maybe fix) any changes: npm run fix From 744e3e8fea223eb4fb115ef0a4d36ad88fc6921a Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Thu, 10 Oct 2019 14:01:43 -0700 Subject: [PATCH 035/662] fix(deps): updats to gcp-metadata with debug option (#811) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1cd8935..8d83d6e8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "base64-js": "^1.3.0", "fast-text-encoding": "^1.0.0", "gaxios": "^2.0.0", - "gcp-metadata": "^3.0.0", + "gcp-metadata": "^3.2.0", "gtoken": "^4.1.0", "jws": "^3.1.5", "lru-cache": "^5.0.0" From 9ace1cdf0d2cbfbf029387e53b81841f66b13e83 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2019 14:17:31 -0700 Subject: [PATCH 036/662] chore: release 5.4.1 (#812) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbef0ac1..51254aba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.4.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.4.0...v5.4.1) (2019-10-10) + + +### Bug Fixes + +* **deps:** updats to gcp-metadata with debug option ([#811](https://www.github.com/googleapis/google-auth-library-nodejs/issues/811)) ([744e3e8](https://www.github.com/googleapis/google-auth-library-nodejs/commit/744e3e8fea223eb4fb115ef0a4d36ad88fc6921a)) + ## [5.4.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.3.0...v5.4.0) (2019-10-08) diff --git a/package.json b/package.json index 8d83d6e8..bed5490b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.4.0", + "version": "5.4.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { From 5c105e63823c149a349aa8722652fa5a0cd6cd1e Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 11 Oct 2019 20:50:46 -0700 Subject: [PATCH 037/662] chore: update pull request template (#813) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- synth.metadata | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 46cd1076..6326e141 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: -- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/{{metadata['repo']['name']}}/issues) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/google-auth-library-nodejs/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) diff --git a/synth.metadata b/synth.metadata index 38a933d0..78df2210 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,5 +1,5 @@ { - "updateTime": "2019-09-18T11:06:57.065713Z", + "updateTime": "2019-10-11T11:06:12.041522Z", "sources": [ { "template": { From e6104c0b2628619f7eafd693a8efbda23d51d94d Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 14 Oct 2019 10:46:21 -0700 Subject: [PATCH 038/662] test: address race condition in tests (#815) --- test/test.googleauth.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index a8259ce3..8a8fe5c8 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -172,8 +172,12 @@ describe('googleauth', () => { return { done: () => { - primary.done(); - secondary.done(); + try { + primary.done(); + secondary.done(); + } catch (_err) { + // secondary can sometimes complete prior to primary. + } }, }; } @@ -187,8 +191,12 @@ describe('googleauth', () => { .replyWithError({code: 'ENOTFOUND'}); return { done: () => { - primary.done(); - secondary.done(); + try { + primary.done(); + secondary.done(); + } catch (_err) { + // secondary can sometimes complete prior to primary. + } }, }; } From d0b7f8b8a1f163da52e231a15ab41c8e92679d99 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 14 Oct 2019 22:39:28 +0300 Subject: [PATCH 039/662] chore(deps): update dependency execa to v3 (#814) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bed5490b..ee61fd43 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "eslint-config-prettier": "^6.0.0", "eslint-plugin-node": "^10.0.0", "eslint-plugin-prettier": "^3.0.0", - "execa": "^2.0.0", + "execa": "^3.0.0", "gts": "^1.0.0", "is-docker": "^2.0.0", "js-green-licenses": "^1.0.0", From 54cf4770f487fd1db48f2444c86109ca97608ed1 Mon Sep 17 00:00:00 2001 From: Eli Skeggs Date: Mon, 14 Oct 2019 15:25:32 -0700 Subject: [PATCH 040/662] feat(refresh): add forceRefreshOnFailure flag for refreshing token on error (#790) --- src/auth/jwtclient.ts | 5 ++++- src/auth/oauth2client.ts | 22 +++++++++++++++++----- src/auth/refreshclient.ts | 5 ++++- test/test.oauth2.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 0374b1f5..ca12b332 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -83,7 +83,10 @@ export class JWT extends OAuth2Client { optionsOrEmail && typeof optionsOrEmail === 'object' ? optionsOrEmail : {email: optionsOrEmail, keyFile, key, keyId, scopes, subject}; - super({eagerRefreshThresholdMillis: opts.eagerRefreshThresholdMillis}); + super({ + eagerRefreshThresholdMillis: opts.eagerRefreshThresholdMillis, + forceRefreshOnFailure: opts.forceRefreshOnFailure, + }); this.email = opts.email; this.keyFile = opts.keyFile; this.key = opts.key; diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 2b04c2a5..65539562 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -354,6 +354,12 @@ export interface RefreshOptions { // milliseconds from expiring". // Defaults to a value of 300000 (5 minutes). eagerRefreshThresholdMillis?: number; + + // Whether to attempt to lazily refresh tokens on 401/403 responses + // even if an attempt is made to refresh the token preemptively based + // on the expiry_date. + // Defaults to false. + forceRefreshOnFailure?: boolean; } export class OAuth2Client extends AuthClient { @@ -375,6 +381,8 @@ export class OAuth2Client extends AuthClient { eagerRefreshThresholdMillis: number; + forceRefreshOnFailure: boolean; + /** * Handles OAuth2 flow for Google APIs. * @@ -402,6 +410,7 @@ export class OAuth2Client extends AuthClient { this.redirectUri = opts.redirectUri; this.eagerRefreshThresholdMillis = opts.eagerRefreshThresholdMillis || 5 * 60 * 1000; + this.forceRefreshOnFailure = !!opts.forceRefreshOnFailure; } protected static readonly GOOGLE_TOKEN_INFO_URL = @@ -896,15 +905,18 @@ export class OAuth2Client extends AuthClient { // - We haven't already retried. It only makes sense to retry once. // - The response was a 401 or a 403 // - The request didn't send a readableStream - // - An access_token and refresh_token were available, but no - // expiry_date was availabe. This can happen when developers stash - // the access_token and refresh_token for later use, but the - // access_token fails on the first try because it's expired. + // - An access_token and refresh_token were available, but either no + // expiry_date was available or the forceRefreshOnFailure flag is set. + // The absent expiry_date case can happen when developers stash the + // access_token and refresh_token for later use, but the access_token + // fails on the first try because it's expired. Some developers may + // choose to enable forceRefreshOnFailure to mitigate time-related + // errors. const mayRequireRefresh = this.credentials && this.credentials.access_token && this.credentials.refresh_token && - !this.credentials.expiry_date; + (!this.credentials.expiry_date || this.forceRefreshOnFailure); const isReadableStream = res.config.data instanceof stream.Readable; const isAuthErr = statusCode === 401 || statusCode === 403; if (!retry && isAuthErr && !isReadableStream && mayRequireRefresh) { diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 4809b39a..802f934a 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -44,7 +44,8 @@ export class UserRefreshClient extends OAuth2Client { optionsOrClientId?: string | UserRefreshClientOptions, clientSecret?: string, refreshToken?: string, - eagerRefreshThresholdMillis?: number + eagerRefreshThresholdMillis?: number, + forceRefreshOnFailure?: boolean ) { const opts = optionsOrClientId && typeof optionsOrClientId === 'object' @@ -54,11 +55,13 @@ export class UserRefreshClient extends OAuth2Client { clientSecret, refreshToken, eagerRefreshThresholdMillis, + forceRefreshOnFailure, }; super({ clientId: opts.clientId, clientSecret: opts.clientSecret, eagerRefreshThresholdMillis: opts.eagerRefreshThresholdMillis, + forceRefreshOnFailure: opts.forceRefreshOnFailure, }); this._refreshToken = opts.refreshToken; } diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 677df9b5..8d6a4912 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1094,6 +1094,32 @@ describe(__filename, () => { done(); }); }); + + it(`should refresh token if the server returns ${code} with forceRefreshOnFailure`, done => { + const client = new OAuth2Client({ + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + redirectUri: REDIRECT_URI, + forceRefreshOnFailure: true, + }); + const scope = nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }); + const scopes = mockExample(); + client.credentials = { + access_token: 'initial-access-token', + refresh_token: 'refresh-token-placeholder', + expiry_date: new Date().getTime() + 500000, + }; + client.request({url: 'http://example.com/access'}, err => { + scope.done(); + scopes[0].done(); + assert.strictEqual('abc123', client.credentials.access_token); + done(); + }); + }); }); it('should not retry requests with streaming data', done => { From a4d9d740362b1374f8f9697c956b2929a3809df3 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2019 16:29:29 -0700 Subject: [PATCH 041/662] chore: release 5.5.0 (#816) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json [ci skip] --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51254aba..cd3fae0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.5.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.4.1...v5.5.0) (2019-10-14) + + +### Features + +* **refresh:** add forceRefreshOnFailure flag for refreshing token on error ([#790](https://www.github.com/googleapis/google-auth-library-nodejs/issues/790)) ([54cf477](https://www.github.com/googleapis/google-auth-library-nodejs/commit/54cf4770f487fd1db48f2444c86109ca97608ed1)) + ### [5.4.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.4.0...v5.4.1) (2019-10-10) diff --git a/package.json b/package.json index ee61fd43..0abb95de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.4.1", + "version": "5.5.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { From 6730698b876eb52889acfead33bc4af52a8a7ba5 Mon Sep 17 00:00:00 2001 From: Logan Hasson Date: Fri, 18 Oct 2019 18:33:52 -0400 Subject: [PATCH 042/662] fix(deps): update gaxios dependency (#817) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0abb95de..bfc6eda8 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "arrify": "^2.0.0", "base64-js": "^1.3.0", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.0.0", + "gaxios": "^2.1.0", "gcp-metadata": "^3.2.0", "gtoken": "^4.1.0", "jws": "^3.1.5", From a46b271947b635377eacbdfcd22ae363ce9260a1 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 22 Oct 2019 10:25:24 -0700 Subject: [PATCH 043/662] fix: don't append x-goog-api-client multiple times (#820) --- src/transporters.ts | 7 +++++-- test/test.transporters.ts | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/transporters.ts b/src/transporters.ts index 2a463a3f..50bb9985 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -70,11 +70,14 @@ export class DefaultTransporter { } // track google-auth-library-nodejs version: const authVersion = `auth/${pkg.version}`; - if (opts.headers['x-goog-api-client']) { + if ( + opts.headers['x-goog-api-client'] && + !opts.headers['x-goog-api-client'].includes(authVersion) + ) { opts.headers[ 'x-goog-api-client' ] = `${opts.headers['x-goog-api-client']} ${authVersion}`; - } else { + } else if (!opts.headers['x-goog-api-client']) { const nodeVersion = process.version.replace(/^v/, ''); opts.headers[ 'x-goog-api-client' diff --git a/test/test.transporters.ts b/test/test.transporters.ts index c4f2b67f..9319a50e 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -71,12 +71,28 @@ it('should append to x-goog-api-client header if it exists', () => { headers: {'x-goog-api-client': 'gdcl/1.0.0'}, url: '', }); - console.info(opts.headers); assert( /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client']) ); }); +// see: https://github.com/googleapis/google-auth-library-nodejs/issues/819 +it('should not append x-goog-api-client header multiple times', () => { + const opts = { + headers: {'x-goog-api-client': 'gdcl/1.0.0'}, + url: '', + }; + let configuredOpts = transporter.configure(opts); + console.info(configuredOpts); + configuredOpts = transporter.configure(opts); + console.info(configuredOpts); + assert( + /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test( + configuredOpts.headers!['x-goog-api-client'] + ) + ); +}); + it('should create a single error from multiple response errors', done => { const firstError = {message: 'Error 1'}; const secondError = {message: 'Error 2'}; From 60cba851196babc72d8a24fde431e6aa40f15e79 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2019 11:12:36 -0700 Subject: [PATCH 044/662] chore: release 5.5.1 (#818) --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3fae0a..9f10146b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.5.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.5.0...v5.5.1) (2019-10-22) + + +### Bug Fixes + +* **deps:** update gaxios dependency ([#817](https://www.github.com/googleapis/google-auth-library-nodejs/issues/817)) ([6730698](https://www.github.com/googleapis/google-auth-library-nodejs/commit/6730698b876eb52889acfead33bc4af52a8a7ba5)) +* don't append x-goog-api-client multiple times ([#820](https://www.github.com/googleapis/google-auth-library-nodejs/issues/820)) ([a46b271](https://www.github.com/googleapis/google-auth-library-nodejs/commit/a46b271947b635377eacbdfcd22ae363ce9260a1)) + ## [5.5.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.4.1...v5.5.0) (2019-10-14) diff --git a/package.json b/package.json index bfc6eda8..05cc021c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.5.0", + "version": "5.5.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 58b24ad9..78b783b4 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.2.1", + "google-auth-library": "^5.5.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 251982f9dd6bff13124ffa44576ff26f234852d1 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 7 Nov 2019 19:57:33 -0800 Subject: [PATCH 045/662] test: collect coverage for src/ folder (#823) --- .nycrc | 1 - synth.metadata | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.nycrc b/.nycrc index 23e32220..36768884 100644 --- a/.nycrc +++ b/.nycrc @@ -10,7 +10,6 @@ "**/docs", "**/samples", "**/scripts", - "**/src/**/v*/**/*.js", "**/protos", "**/test", ".jsdoc.js", diff --git a/synth.metadata b/synth.metadata index 78df2210..596915a7 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,11 +1,11 @@ { - "updateTime": "2019-10-11T11:06:12.041522Z", + "updateTime": "2019-10-30T22:14:34.735126Z", "sources": [ { "template": { "name": "node_library", "origin": "synthtool.gcp", - "version": "2019.5.2" + "version": "2019.10.17" } } ] From f44030234bf11e0fa3360554f84406b9b30b4008 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 8 Nov 2019 05:58:09 +0200 Subject: [PATCH 046/662] chore(deps): update dependency typescript to ~3.7.0 (#824) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05cc021c..375e5d77 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "source-map-support": "^0.5.6", "tmp": "^0.1.0", "ts-loader": "^6.0.0", - "typescript": "~3.6.0", + "typescript": "~3.7.0", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" }, From 2c0411708761cc7debdda1af1e593d82cb4aed31 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 8 Nov 2019 18:38:05 +0100 Subject: [PATCH 047/662] fix(deps): update dependency puppeteer to v2 (#821) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 375e5d77..68c7d894 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "null-loader": "^3.0.0", "nyc": "^14.1.1", "prettier": "^1.13.4", - "puppeteer": "^1.11.0", + "puppeteer": "^2.0.0", "sinon": "^7.0.0", "source-map-support": "^0.5.6", "tmp": "^0.1.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 4ed0e71e..7ed09726 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^5.0.0", - "puppeteer": "^1.0.0" + "puppeteer": "^2.0.0" } } From 4a5da6d5073553beea527c1d0e3afe1056415e79 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 15 Nov 2019 10:20:09 -0800 Subject: [PATCH 048/662] chore: add gitattributes to kokoro --- .kokoro/.gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .kokoro/.gitattributes diff --git a/.kokoro/.gitattributes b/.kokoro/.gitattributes new file mode 100644 index 00000000..87acd4f4 --- /dev/null +++ b/.kokoro/.gitattributes @@ -0,0 +1 @@ +* linguist-generated=true From 60a40012ae17b5e21aeec37cdc854dfee0b81588 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 20 Nov 2019 09:06:31 -0800 Subject: [PATCH 049/662] build: adds jsdoc-region-tag plugin for doc generation (#827) --- .jsdoc.js | 3 ++- README.md | 2 +- synth.metadata | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.jsdoc.js b/.jsdoc.js index 4c8c69ee..cea5c2e0 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -26,7 +26,8 @@ module.exports = { destination: './docs/' }, plugins: [ - 'plugins/markdown' + 'plugins/markdown', + 'jsdoc-region-tag' ], source: { excludePattern: '(^|\\/|\\\\)[._]', diff --git a/README.md b/README.md index 65cbaa7b..b1780507 100644 --- a/README.md +++ b/README.md @@ -364,4 +364,4 @@ This library is licensed under Apache 2.0. Full license text is available in [LI [snyk-image]: https://snyk.io/test/github/googleapis/google-auth-library-nodejs/badge.svg [snyk-url]: https://snyk.io/test/github/googleapis/google-auth-library-nodejs [stackoverflow]: http://stackoverflow.com/questions/tagged/google-auth-library-nodejs -[devconsole]: https://console.developer.google.com +[devconsole]: https://console.cloud.google.com/ diff --git a/synth.metadata b/synth.metadata index 596915a7..75944fdc 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,5 +1,5 @@ { - "updateTime": "2019-10-30T22:14:34.735126Z", + "updateTime": "2019-11-14T12:07:29.320036Z", "sources": [ { "template": { From 558677fd90d3451e9ac4bf6d0b98907e3313f287 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 20 Nov 2019 09:12:04 -0800 Subject: [PATCH 050/662] fix(docs): add jsdoc-region-tag plugin (#826) From 3240d16f05171781fe6d70d64c476bceb25805a5 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 20 Nov 2019 15:41:23 -0800 Subject: [PATCH 051/662] feat: set x-goog-user-project header, with quota_project from default credentials (#829) --- package.json | 2 +- src/auth/credentials.ts | 1 + src/auth/googleauth.ts | 16 +++++++- .../application_default_credentials.json | 8 ++++ test/test.googleauth.ts | 38 +++++++++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/.config/gcloud/application_default_credentials.json diff --git a/package.json b/package.json index 68c7d894..0d979f13 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "eslint-plugin-node": "^10.0.0", "eslint-plugin-prettier": "^3.0.0", "execa": "^3.0.0", - "gts": "^1.0.0", + "gts": "^1.1.2", "is-docker": "^2.0.0", "js-green-licenses": "^1.0.0", "karma": "^4.0.0", diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index d5cae57a..83feb44f 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -39,6 +39,7 @@ export interface JWTInput { client_id?: string; client_secret?: string; refresh_token?: string; + quota_project?: string; } export interface CredentialBody { diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 3ca8b9f4..76813a94 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -745,7 +745,21 @@ export class GoogleAuth { */ async getRequestHeaders(url?: string) { const client = await this.getClient(); - return client.getRequestHeaders(url); + let headers = client.getRequestHeaders(url); + // quota_project, stored in application_default_credentials.json, is set in + // the x-goog-user-project header, to indicate an alternate account for + // billing and quota: + if (this.jsonContent?.quota_project) { + // If x-goog-user-project has explicitly been set in the client headers, + // it takes precedence. + headers = Object.assign( + { + 'x-goog-user-project': this.jsonContent.quota_project, + }, + headers + ); + } + return headers; } /** diff --git a/test/fixtures/.config/gcloud/application_default_credentials.json b/test/fixtures/.config/gcloud/application_default_credentials.json new file mode 100644 index 00000000..36144859 --- /dev/null +++ b/test/fixtures/.config/gcloud/application_default_credentials.json @@ -0,0 +1,8 @@ +{ + "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com", + "client_secret": "privatekey", + "refresh_token": "refreshtoken", + "type": "authorized_user", + "project_id": "my-project", + "quota_project": "my-quota-project" +} diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 8a8fe5c8..f43e7b2e 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1444,4 +1444,42 @@ describe('googleauth', () => { /Passing options to getClient is forbidden in v5.0.0/ ); }); + + it('getRequestHeaders() populates x-goog-user-project with quota_project if present', async () => { + // Fake a home directory in our fixtures path. + mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); + mockEnvVar('HOME', './test/fixtures'); + mockEnvVar('APPDATA', './test/fixtures/.config'); + // The first time auth.getClient() is called /token endpoint is used to + // fetch a JWT. + const req = nock('https://oauth2.googleapis.com') + .post('/token') + .reply(200, {}); + + const auth = new GoogleAuth(); + const headers = await auth.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + req.done(); + }); + + it('getRequestHeaders() does not populate x-goog-user-project if quota_project is not present', async () => { + // Fake a home directory in our fixtures path. + mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); + mockEnvVar('HOME', './test/fixtures'); + mockEnvVar('APPDATA', './test/fixtures/.config'); + // The first time auth.getClient() is called /token endpoint is used to + // fetch a JWT. + const req = nock('https://oauth2.googleapis.com') + .post('/token') + .reply(200, {}); + + const auth = new GoogleAuth(); + // Force jsonContent to load, and then remove the quota_project parameter. + await auth.getClient(); + delete auth.jsonContent!.quota_project; + + const headers = await auth.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], undefined); + req.done(); + }); }); From 3646b7f9deb296aaff602dd2168ce93f014ce840 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Thu, 21 Nov 2019 10:58:23 -0800 Subject: [PATCH 052/662] fix: expand on x-goog-user-project to handle auth.getClient() (#831) --- src/auth/googleauth.ts | 16 +-------- src/auth/jwtclient.ts | 1 + src/auth/oauth2client.ts | 16 +++++++-- src/auth/refreshclient.ts | 8 ++++- .../application_default_credentials.json | 7 ++++ .../application_default_credentials.json | 0 test/test.googleauth.ts | 34 +++++++++++++------ test/test.refresh.ts | 19 +++++++++++ 8 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 test/fixtures/config-no-quota/.config/gcloud/application_default_credentials.json rename test/fixtures/{ => config-with-quota}/.config/gcloud/application_default_credentials.json (100%) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 76813a94..3ca8b9f4 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -745,21 +745,7 @@ export class GoogleAuth { */ async getRequestHeaders(url?: string) { const client = await this.getClient(); - let headers = client.getRequestHeaders(url); - // quota_project, stored in application_default_credentials.json, is set in - // the x-goog-user-project header, to indicate an alternate account for - // billing and quota: - if (this.jsonContent?.quota_project) { - // If x-goog-user-project has explicitly been set in the client headers, - // it takes precedence. - headers = Object.assign( - { - 'x-goog-user-project': this.jsonContent.quota_project, - }, - headers - ); - } - return headers; + return client.getRequestHeaders(url); } /** diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index ca12b332..05987f44 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -267,6 +267,7 @@ export class JWT extends OAuth2Client { this.key = json.private_key; this.keyId = json.private_key_id; this.projectId = json.project_id; + this.quotaProject = json.quota_project; } /** diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 65539562..e4182e0c 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -28,7 +28,7 @@ import * as messages from '../messages'; import {BodyResponseCallback} from '../transporters'; import {AuthClient} from './authclient'; -import {CredentialRequest, Credentials} from './credentials'; +import {CredentialRequest, Credentials, JWTInput} from './credentials'; import {LoginTicket, TokenPayload} from './loginticket'; export interface Certificates { @@ -367,6 +367,7 @@ export class OAuth2Client extends AuthClient { private certificateCache: Certificates = {}; private certificateExpiry: Date | null = null; private certificateCacheFormat: CertificateFormat = CertificateFormat.PEM; + protected quotaProject?: string; protected refreshTokenPromises = new Map>(); // TODO: refactor tests to make this private @@ -742,8 +743,17 @@ export class OAuth2Client extends AuthClient { * @param url The optional url being authorized */ async getRequestHeaders(url?: string): Promise { - const res = await this.getRequestMetadataAsync(url); - return res.headers; + const headers = (await this.getRequestMetadataAsync(url)).headers; + // quota_project, stored in application_default_credentials.json, is set in + // the x-goog-user-project header, to indicate an alternate account for + // billing and quota: + if ( + !headers['x-goog-user-project'] && // don't override a value the user sets. + this.quotaProject + ) { + headers['x-goog-user-project'] = this.quotaProject; + } + return headers; } protected async getRequestMetadataAsync( diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 802f934a..18c381ac 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -16,7 +16,12 @@ import * as stream from 'stream'; import {JWTInput} from './credentials'; -import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; +import { + Headers, + GetTokenResponse, + OAuth2Client, + RefreshOptions, +} from './oauth2client'; export interface UserRefreshClientOptions extends RefreshOptions { clientId?: string; @@ -112,6 +117,7 @@ export class UserRefreshClient extends OAuth2Client { this._clientSecret = json.client_secret; this._refreshToken = json.refresh_token; this.credentials.refresh_token = json.refresh_token; + this.quotaProject = json.quota_project; } /** diff --git a/test/fixtures/config-no-quota/.config/gcloud/application_default_credentials.json b/test/fixtures/config-no-quota/.config/gcloud/application_default_credentials.json new file mode 100644 index 00000000..89fa7c76 --- /dev/null +++ b/test/fixtures/config-no-quota/.config/gcloud/application_default_credentials.json @@ -0,0 +1,7 @@ +{ + "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com", + "client_secret": "privatekey", + "refresh_token": "refreshtoken", + "type": "authorized_user", + "project_id": "my-project" +} diff --git a/test/fixtures/.config/gcloud/application_default_credentials.json b/test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json similarity index 100% rename from test/fixtures/.config/gcloud/application_default_credentials.json rename to test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index f43e7b2e..75838675 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1445,11 +1445,11 @@ describe('googleauth', () => { ); }); - it('getRequestHeaders() populates x-goog-user-project with quota_project if present', async () => { + it('getRequestHeaders populates x-goog-user-project with quota_project if present', async () => { // Fake a home directory in our fixtures path. mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); - mockEnvVar('HOME', './test/fixtures'); - mockEnvVar('APPDATA', './test/fixtures/.config'); + mockEnvVar('HOME', './test/fixtures/config-with-quota'); + mockEnvVar('APPDATA', './test/fixtures/config-with-quota/.config'); // The first time auth.getClient() is called /token endpoint is used to // fetch a JWT. const req = nock('https://oauth2.googleapis.com') @@ -1462,11 +1462,11 @@ describe('googleauth', () => { req.done(); }); - it('getRequestHeaders() does not populate x-goog-user-project if quota_project is not present', async () => { + it('getRequestHeaders does not populate x-goog-user-project if quota_project is not present', async () => { // Fake a home directory in our fixtures path. mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); - mockEnvVar('HOME', './test/fixtures'); - mockEnvVar('APPDATA', './test/fixtures/.config'); + mockEnvVar('HOME', './test/fixtures/config-no-quota'); + mockEnvVar('APPDATA', './test/fixtures/config-no-quota/.config'); // The first time auth.getClient() is called /token endpoint is used to // fetch a JWT. const req = nock('https://oauth2.googleapis.com') @@ -1474,12 +1474,26 @@ describe('googleauth', () => { .reply(200, {}); const auth = new GoogleAuth(); - // Force jsonContent to load, and then remove the quota_project parameter. - await auth.getClient(); - delete auth.jsonContent!.quota_project; - const headers = await auth.getRequestHeaders(); assert.strictEqual(headers['x-goog-user-project'], undefined); req.done(); }); + + it('getRequestHeaders populates x-goog-user-project when called on returned client', async () => { + // Fake a home directory in our fixtures path. + mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); + mockEnvVar('HOME', './test/fixtures/config-with-quota'); + mockEnvVar('APPDATA', './test/fixtures/config-with-quota/.config'); + // The first time auth.getClient() is called /token endpoint is used to + // fetch a JWT. + const req = nock('https://oauth2.googleapis.com') + .post('/token') + .reply(200, {}); + + const auth = new GoogleAuth(); + const client = await auth.getClient(); + const headers = await client.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + req.done(); + }); }); diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 106a4d97..16ee5a8d 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -16,6 +16,7 @@ import * as assert from 'assert'; import * as fs from 'fs'; +import * as nock from 'nock'; import {UserRefreshClient} from '../src'; // Creates a standard JSON credentials object for testing. @@ -123,3 +124,21 @@ it('fromStream should read the stream and create a UserRefreshClient', done => { done(); }); }); + +it('getRequestHeaders should populate x-goog-user-project header if quota_project present', async () => { + // The first time auth.getRequestHeaders() is called /token endpoint is used to + // fetch a JWT. + const req = nock('https://oauth2.googleapis.com') + .post('/token') + .reply(200, {}); + + // Fake loading default credentials with quota project set: + const stream = fs.createReadStream( + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + ); + const refresh = new UserRefreshClient(); + await refresh.fromStream(stream); + + const headers = await refresh.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); +}); From 2327cc2d2cb13b8babf6402feeddfad515b5a8c2 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 25 Nov 2019 08:54:39 -0800 Subject: [PATCH 053/662] chore: update license headers --- browser-test/fixtures/keys.ts | 28 +++++++++++++--------------- browser-test/test.crypto.ts | 28 +++++++++++++--------------- browser-test/test.oauth2.ts | 28 +++++++++++++--------------- karma.conf.js | 28 +++++++++++++--------------- samples/test/jwt.test.js | 28 +++++++++++++--------------- src/auth/authclient.ts | 28 +++++++++++++--------------- src/auth/computeclient.ts | 28 +++++++++++++--------------- src/auth/credentials.ts | 28 +++++++++++++--------------- src/auth/envDetect.ts | 28 +++++++++++++--------------- src/auth/googleauth.ts | 28 +++++++++++++--------------- src/auth/iam.ts | 28 +++++++++++++--------------- src/auth/jwtaccess.ts | 28 +++++++++++++--------------- src/auth/jwtclient.ts | 28 +++++++++++++--------------- src/auth/loginticket.ts | 28 +++++++++++++--------------- src/auth/oauth2client.ts | 28 +++++++++++++--------------- src/auth/refreshclient.ts | 28 +++++++++++++--------------- src/crypto/browser/crypto.ts | 28 +++++++++++++--------------- src/crypto/crypto.ts | 28 +++++++++++++--------------- src/crypto/node/crypto.ts | 28 +++++++++++++--------------- src/index.ts | 28 +++++++++++++--------------- src/messages.ts | 28 +++++++++++++--------------- src/options.ts | 28 +++++++++++++--------------- src/transporters.ts | 28 +++++++++++++--------------- system-test/test.kitchen.ts | 28 +++++++++++++--------------- test/test.compute.ts | 28 +++++++++++++--------------- test/test.googleauth.ts | 28 +++++++++++++--------------- test/test.iam.ts | 28 +++++++++++++--------------- test/test.index.ts | 28 +++++++++++++--------------- test/test.jwt.ts | 28 +++++++++++++--------------- test/test.jwtaccess.ts | 28 +++++++++++++--------------- test/test.loginticket.ts | 28 +++++++++++++--------------- test/test.oauth2.ts | 28 +++++++++++++--------------- test/test.refresh.ts | 28 +++++++++++++--------------- test/test.transporters.ts | 28 +++++++++++++--------------- webpack-tests.config.js | 28 +++++++++++++--------------- webpack.config.js | 28 +++++++++++++--------------- 36 files changed, 468 insertions(+), 540 deletions(-) diff --git a/browser-test/fixtures/keys.ts b/browser-test/fixtures/keys.ts index f5785867..cdd5f770 100644 --- a/browser-test/fixtures/keys.ts +++ b/browser-test/fixtures/keys.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. // The following private and public keys were copied from JWK RFC 7517: // https://tools.ietf.org/html/rfc7517 diff --git a/browser-test/test.crypto.ts b/browser-test/test.crypto.ts index 5c34b8be..c152c5eb 100644 --- a/browser-test/test.crypto.ts +++ b/browser-test/test.crypto.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import * as base64js from 'base64-js'; import {assert} from 'chai'; diff --git a/browser-test/test.oauth2.ts b/browser-test/test.oauth2.ts index 8a1313d0..a87429a1 100644 --- a/browser-test/test.oauth2.ts +++ b/browser-test/test.oauth2.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import * as base64js from 'base64-js'; import {assert} from 'chai'; diff --git a/karma.conf.js b/karma.conf.js index 3cc59041..5b812100 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. // Karma configuration // Use `npm run browser-test` to run browser tests with Karma. diff --git a/samples/test/jwt.test.js b/samples/test/jwt.test.js index 18909c33..5e613682 100644 --- a/samples/test/jwt.test.js +++ b/samples/test/jwt.test.js @@ -1,18 +1,16 @@ -/** - * Copyright 2018 Google LLC. All Rights Reserved. - * - * 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. - */ +// Copyright 2018 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. const cp = require('child_process'); const {assert} = require('chai'); diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 5de4f480..ab10eac4 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2012 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2012 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. import {EventEmitter} from 'events'; import {GaxiosOptions, GaxiosPromise} from 'gaxios'; diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 1386640a..1b97bef6 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2013 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2013 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. import arrify = require('arrify'); import {GaxiosError} from 'gaxios'; diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index 83feb44f..94c01625 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2014 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2014 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. export interface Credentials { refresh_token?: string | null; diff --git a/src/auth/envDetect.ts b/src/auth/envDetect.ts index e3b15374..b5c908d5 100644 --- a/src/auth/envDetect.ts +++ b/src/auth/envDetect.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2018 Google LLC. All Rights Reserved. - * - * 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. - */ +// Copyright 2018 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. import * as gcpMetadata from 'gcp-metadata'; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 3ca8b9f4..64a9ed9c 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import {exec} from 'child_process'; import * as fs from 'fs'; diff --git a/src/auth/iam.ts b/src/auth/iam.ts index e34efffd..cfb8fefe 100644 --- a/src/auth/iam.ts +++ b/src/auth/iam.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2014 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2014 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. import * as messages from '../messages'; diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index a24d4d77..03cdd6d1 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2015 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2015 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. import * as jws from 'jws'; import * as LRU from 'lru-cache'; diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 05987f44..41a18e6d 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2013 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2013 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. import {GoogleToken} from 'gtoken'; import * as stream from 'stream'; diff --git a/src/auth/loginticket.ts b/src/auth/loginticket.ts index 0853b345..1a86d2ca 100644 --- a/src/auth/loginticket.ts +++ b/src/auth/loginticket.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2014 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2014 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. export class LoginTicket { private envelope?: string; diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index e4182e0c..a1f3b22b 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import { GaxiosError, diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 18c381ac..4e0f7521 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2015 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2015 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. import * as stream from 'stream'; import {JWTInput} from './credentials'; diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index 9792557a..f84191b6 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. // This file implements crypto functions we need using in-browser // SubtleCrypto interface `window.crypto.subtle`. diff --git a/src/crypto/crypto.ts b/src/crypto/crypto.ts index 1a73dd30..45ca5c9b 100644 --- a/src/crypto/crypto.ts +++ b/src/crypto/crypto.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import {BrowserCrypto} from './browser/crypto'; import {NodeCrypto} from './node/crypto'; diff --git a/src/crypto/node/crypto.ts b/src/crypto/node/crypto.ts index bfec404b..215ea7a4 100644 --- a/src/crypto/node/crypto.ts +++ b/src/crypto/node/crypto.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import * as crypto from 'crypto'; import {Crypto} from '../crypto'; diff --git a/src/index.ts b/src/index.ts index 78401965..316216a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2017 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. import {GoogleAuth} from './auth/googleauth'; export {Compute, ComputeOptions} from './auth/computeclient'; diff --git a/src/messages.ts b/src/messages.ts index 9d90cc59..a613684f 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2018 Google LLC. All Rights Reserved. - * - * 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. - */ +// Copyright 2018 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. export enum WarningTypes { WARNING = 'Warning', diff --git a/src/options.ts b/src/options.ts index 6742782a..eab5d4ba 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2017 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. // Accepts an options object passed from the user to the API. In the // previous version of the API, it referred to a `Request` options object. diff --git a/src/transporters.ts b/src/transporters.ts index 50bb9985..e9feb5be 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import { GaxiosError, diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index f12ed35a..3ef001f3 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import * as assert from 'assert'; import * as execa from 'execa'; diff --git a/test/test.compute.ts b/test/test.compute.ts index ba47c0df..3650d6be 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2013 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2013 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. import * as assert from 'assert'; const assertRejects = require('assert-rejects'); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 75838675..5777c6fb 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2014 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2014 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. import * as assert from 'assert'; const assertRejects = require('assert-rejects'); diff --git a/test/test.iam.ts b/test/test.iam.ts index b37f1342..c09d5fb2 100644 --- a/test/test.iam.ts +++ b/test/test.iam.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2013 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2013 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. import * as assert from 'assert'; import * as sinon from 'sinon'; diff --git a/test/test.index.ts b/test/test.index.ts index faa8a321..28296419 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2017 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. import * as assert from 'assert'; import * as gal from '../src'; diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 755c8ef7..838f3de6 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2013 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2013 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. import * as assert from 'assert'; import * as fs from 'fs'; diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index a8f6ee2d..80aa5fc9 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2013 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2013 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. import * as assert from 'assert'; import * as fs from 'fs'; diff --git a/test/test.loginticket.ts b/test/test.loginticket.ts index 9a47a1a6..fc56f4dd 100644 --- a/test/test.loginticket.ts +++ b/test/test.loginticket.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2015 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2015 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. import * as assert from 'assert'; import {LoginTicket} from '../src/auth/loginticket'; diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 8d6a4912..aa84c7bc 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. import * as assert from 'assert'; const assertRejects = require('assert-rejects'); diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 16ee5a8d..e08b68df 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2013 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2013 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. import * as assert from 'assert'; import * as fs from 'fs'; diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 9319a50e..0f779afd 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -1,18 +1,16 @@ -/** - * Copyright 2013 Google Inc. All Rights Reserved. - * - * 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. - */ +// Copyright 2013 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. import * as assert from 'assert'; import {GaxiosOptions} from 'gaxios'; diff --git a/webpack-tests.config.js b/webpack-tests.config.js index 35a5b56e..c473e693 100644 --- a/webpack-tests.config.js +++ b/webpack-tests.config.js @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. // This configuration file is used for browser testing with Karma. // See karma.conf.js for details. diff --git a/webpack.config.js b/webpack.config.js index 9ab50e9e..e9b3dde4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,18 +1,16 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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. // Use `npm run webpack` to produce Webpack bundle for this library. From 8933966659f3b07f5454a2756fa52d92fea147d2 Mon Sep 17 00:00:00 2001 From: Chris Broadfoot Date: Mon, 25 Nov 2019 12:52:30 -0500 Subject: [PATCH 054/662] fix: use quota_project_id field instead of quota_project (#832) --- src/auth/credentials.ts | 2 +- src/auth/jwtclient.ts | 2 +- src/auth/oauth2client.ts | 8 ++++---- src/auth/refreshclient.ts | 2 +- .../.config/gcloud/application_default_credentials.json | 2 +- test/test.refresh.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index 94c01625..a9ee2e19 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -37,7 +37,7 @@ export interface JWTInput { client_id?: string; client_secret?: string; refresh_token?: string; - quota_project?: string; + quota_project_id?: string; } export interface CredentialBody { diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 41a18e6d..a2153a27 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -265,7 +265,7 @@ export class JWT extends OAuth2Client { this.key = json.private_key; this.keyId = json.private_key_id; this.projectId = json.project_id; - this.quotaProject = json.quota_project; + this.quotaProjectId = json.quota_project_id; } /** diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index a1f3b22b..31af6a9c 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -365,7 +365,7 @@ export class OAuth2Client extends AuthClient { private certificateCache: Certificates = {}; private certificateExpiry: Date | null = null; private certificateCacheFormat: CertificateFormat = CertificateFormat.PEM; - protected quotaProject?: string; + protected quotaProjectId?: string; protected refreshTokenPromises = new Map>(); // TODO: refactor tests to make this private @@ -742,14 +742,14 @@ export class OAuth2Client extends AuthClient { */ async getRequestHeaders(url?: string): Promise { const headers = (await this.getRequestMetadataAsync(url)).headers; - // quota_project, stored in application_default_credentials.json, is set in + // quota_project_id, stored in application_default_credentials.json, is set in // the x-goog-user-project header, to indicate an alternate account for // billing and quota: if ( !headers['x-goog-user-project'] && // don't override a value the user sets. - this.quotaProject + this.quotaProjectId ) { - headers['x-goog-user-project'] = this.quotaProject; + headers['x-goog-user-project'] = this.quotaProjectId; } return headers; } diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 4e0f7521..7175f098 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -115,7 +115,7 @@ export class UserRefreshClient extends OAuth2Client { this._clientSecret = json.client_secret; this._refreshToken = json.refresh_token; this.credentials.refresh_token = json.refresh_token; - this.quotaProject = json.quota_project; + this.quotaProjectId = json.quota_project_id; } /** diff --git a/test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json b/test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json index 36144859..1aa818da 100644 --- a/test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json +++ b/test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json @@ -4,5 +4,5 @@ "refresh_token": "refreshtoken", "type": "authorized_user", "project_id": "my-project", - "quota_project": "my-quota-project" + "quota_project_id": "my-quota-project" } diff --git a/test/test.refresh.ts b/test/test.refresh.ts index e08b68df..9a5fcb1c 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -123,7 +123,7 @@ it('fromStream should read the stream and create a UserRefreshClient', done => { }); }); -it('getRequestHeaders should populate x-goog-user-project header if quota_project present', async () => { +it('getRequestHeaders should populate x-goog-user-project header if quota_project_id present', async () => { // The first time auth.getRequestHeaders() is called /token endpoint is used to // fetch a JWT. const req = nock('https://oauth2.googleapis.com') From 5a068fb8f5a3827ab70404f1d9699a97f962bdad Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 2 Dec 2019 11:15:03 -0800 Subject: [PATCH 055/662] feat: populate x-goog-user-project for requestAsync (#837) --- src/auth/oauth2client.ts | 28 ++++++++------ test/test.googleauth.ts | 80 ++++++++++++++++++++++++---------------- 2 files changed, 64 insertions(+), 44 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 31af6a9c..2187461d 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -742,15 +742,6 @@ export class OAuth2Client extends AuthClient { */ async getRequestHeaders(url?: string): Promise { const headers = (await this.getRequestMetadataAsync(url)).headers; - // quota_project_id, stored in application_default_credentials.json, is set in - // the x-goog-user-project header, to indicate an alternate account for - // billing and quota: - if ( - !headers['x-goog-user-project'] && // don't override a value the user sets. - this.quotaProjectId - ) { - headers['x-goog-user-project'] = this.quotaProjectId; - } return headers; } @@ -793,9 +784,20 @@ export class OAuth2Client extends AuthClient { credentials.token_type = credentials.token_type || 'Bearer'; tokens.refresh_token = credentials.refresh_token; this.credentials = tokens; - const headers = { + const headers: {[index: string]: string} = { Authorization: credentials.token_type + ' ' + tokens.access_token, }; + + // quota_project_id, stored in application_default_credentials.json, is set in + // the x-goog-user-project header, to indicate an alternate account for + // billing and quota: + if ( + !headers['x-goog-user-project'] && // don't override a value the user sets. + this.quotaProjectId + ) { + headers['x-goog-user-project'] = this.quotaProjectId; + } + return {headers, res: r.res}; } @@ -896,12 +898,14 @@ export class OAuth2Client extends AuthClient { let r2: GaxiosResponse; try { const r = await this.getRequestMetadataAsync(opts.url); + opts.headers = opts.headers || {}; + if (r.headers?.['x-goog-user-project']) { + opts.headers['x-goog-user-project'] = r.headers['x-goog-user-project']; + } if (r.headers && r.headers.Authorization) { - opts.headers = opts.headers || {}; opts.headers.Authorization = r.headers.Authorization; } if (this.apiKey) { - opts.headers = opts.headers || {}; opts.headers['X-Goog-Api-Key'] = this.apiKey; } r2 = await this.transporter.request(opts); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 5777c6fb..4ae37080 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1444,54 +1444,70 @@ describe('googleauth', () => { }); it('getRequestHeaders populates x-goog-user-project with quota_project if present', async () => { - // Fake a home directory in our fixtures path. - mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); - mockEnvVar('HOME', './test/fixtures/config-with-quota'); - mockEnvVar('APPDATA', './test/fixtures/config-with-quota/.config'); - // The first time auth.getClient() is called /token endpoint is used to - // fetch a JWT. - const req = nock('https://oauth2.googleapis.com') - .post('/token') - .reply(200, {}); - + const tokenReq = mockApplicationDefaultCredentials( + './test/fixtures/config-with-quota' + ); const auth = new GoogleAuth(); const headers = await auth.getRequestHeaders(); assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); - req.done(); + tokenReq.done(); }); it('getRequestHeaders does not populate x-goog-user-project if quota_project is not present', async () => { - // Fake a home directory in our fixtures path. - mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); - mockEnvVar('HOME', './test/fixtures/config-no-quota'); - mockEnvVar('APPDATA', './test/fixtures/config-no-quota/.config'); - // The first time auth.getClient() is called /token endpoint is used to - // fetch a JWT. - const req = nock('https://oauth2.googleapis.com') - .post('/token') - .reply(200, {}); - + const tokenReq = mockApplicationDefaultCredentials( + './test/fixtures/config-no-quota' + ); const auth = new GoogleAuth(); const headers = await auth.getRequestHeaders(); assert.strictEqual(headers['x-goog-user-project'], undefined); - req.done(); + tokenReq.done(); }); it('getRequestHeaders populates x-goog-user-project when called on returned client', async () => { + const tokenReq = mockApplicationDefaultCredentials( + './test/fixtures/config-with-quota' + ); + const auth = new GoogleAuth(); + const client = await auth.getClient(); + const headers = await client.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + tokenReq.done(); + }); + + it('populates x-goog-user-project when request is made', async () => { + const tokenReq = mockApplicationDefaultCredentials( + './test/fixtures/config-with-quota' + ); + const auth = new GoogleAuth(); + const client = await auth.getClient(); + const apiReq = nock(BASE_URL) + .post(ENDPOINT) + .reply(function(uri) { + assert.strictEqual( + this.req.headers['x-goog-user-project'][0], + 'my-quota-project' + ); + return [200, RESPONSE_BODY]; + }); + const res = await client.request({ + url: BASE_URL + ENDPOINT, + method: 'POST', + data: {test: true}, + }); + assert.strictEqual(RESPONSE_BODY, res.data); + tokenReq.done(); + apiReq.done(); + }); + + function mockApplicationDefaultCredentials(path: string) { // Fake a home directory in our fixtures path. mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); - mockEnvVar('HOME', './test/fixtures/config-with-quota'); - mockEnvVar('APPDATA', './test/fixtures/config-with-quota/.config'); + mockEnvVar('HOME', path); + mockEnvVar('APPDATA', `${path}/.config`); // The first time auth.getClient() is called /token endpoint is used to // fetch a JWT. - const req = nock('https://oauth2.googleapis.com') + return nock('https://oauth2.googleapis.com') .post('/token') .reply(200, {}); - - const auth = new GoogleAuth(); - const client = await auth.getClient(); - const headers = await client.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); - req.done(); - }); + } }); From a5e0390a1bea9e1fd56210da08078a70d6017da5 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2019 11:51:45 -0800 Subject: [PATCH 056/662] chore: release 5.6.0 (#830) --- CHANGELOG.md | 16 ++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f10146b..f66c32bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.6.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.5.1...v5.6.0) (2019-12-02) + + +### Features + +* populate x-goog-user-project for requestAsync ([#837](https://www.github.com/googleapis/google-auth-library-nodejs/issues/837)) ([5a068fb](https://www.github.com/googleapis/google-auth-library-nodejs/commit/5a068fb8f5a3827ab70404f1d9699a97f962bdad)) +* set x-goog-user-project header, with quota_project from default credentials ([#829](https://www.github.com/googleapis/google-auth-library-nodejs/issues/829)) ([3240d16](https://www.github.com/googleapis/google-auth-library-nodejs/commit/3240d16f05171781fe6d70d64c476bceb25805a5)) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v2 ([#821](https://www.github.com/googleapis/google-auth-library-nodejs/issues/821)) ([2c04117](https://www.github.com/googleapis/google-auth-library-nodejs/commit/2c0411708761cc7debdda1af1e593d82cb4aed31)) +* **docs:** add jsdoc-region-tag plugin ([#826](https://www.github.com/googleapis/google-auth-library-nodejs/issues/826)) ([558677f](https://www.github.com/googleapis/google-auth-library-nodejs/commit/558677fd90d3451e9ac4bf6d0b98907e3313f287)) +* expand on x-goog-user-project to handle auth.getClient() ([#831](https://www.github.com/googleapis/google-auth-library-nodejs/issues/831)) ([3646b7f](https://www.github.com/googleapis/google-auth-library-nodejs/commit/3646b7f9deb296aaff602dd2168ce93f014ce840)) +* use quota_project_id field instead of quota_project ([#832](https://www.github.com/googleapis/google-auth-library-nodejs/issues/832)) ([8933966](https://www.github.com/googleapis/google-auth-library-nodejs/commit/8933966659f3b07f5454a2756fa52d92fea147d2)) + ### [5.5.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.5.0...v5.5.1) (2019-10-22) diff --git a/package.json b/package.json index 0d979f13..3f43bc25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.5.1", + "version": "5.6.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 78b783b4..f0508e72 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.5.1", + "google-auth-library": "^5.6.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 04dae9c271f0099025188489c61fd245d482832b Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Tue, 3 Dec 2019 12:13:54 -0800 Subject: [PATCH 057/662] fix(docs): improve types and docs for generateCodeVerifierAsync (#840) --- src/auth/oauth2client.ts | 25 +++++++++++++++++++++++-- src/index.ts | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 2187461d..3a895c39 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -29,6 +29,24 @@ import {AuthClient} from './authclient'; import {CredentialRequest, Credentials, JWTInput} from './credentials'; import {LoginTicket, TokenPayload} from './loginticket'; +/** + * The results from the `generateCodeVerifierAsync` method. To learn more, + * See the sample: + * https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2-codeVerifier.js + */ +export interface CodeVerifierResults { + /** + * The code verifier that will be used when calling `getToken` to obtain a new + * access token. + */ + codeVerifier: string; + /** + * The code_challenge that should be sent with the `generateAuthUrl` call + * to obtain a verifiable authentication url. + */ + codeChallenge?: string; +} + export interface Certificates { [index: string]: string | JwkCertificate; } @@ -485,7 +503,7 @@ export class OAuth2Client extends AuthClient { return rootUrl + '?' + querystring.stringify(opts); } - generateCodeVerifier() { + generateCodeVerifier(): void { // To make the code compatible with browser SubtleCrypto we need to make // this method async. throw new Error( @@ -497,8 +515,11 @@ export class OAuth2Client extends AuthClient { * Convenience method to automatically generate a code_verifier, and it's * resulting SHA256. If used, this must be paired with a S256 * code_challenge_method. + * + * For a full example see: + * https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2-codeVerifier.js */ - async generateCodeVerifierAsync() { + async generateCodeVerifierAsync(): Promise { // base64 encoding uses 6 bits per character, and we want to generate128 // characters. 6*128/8 = 96. const crypto = createCrypto(); diff --git a/src/index.ts b/src/index.ts index 316216a8..39d98b3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ export {JWT, JWTOptions} from './auth/jwtclient'; export { Certificates, CodeChallengeMethod, + CodeVerifierResults, GenerateAuthUrlOpts, GetTokenOptions, OAuth2Client, From a9c6e9284efe8102974c57c9824ed6275d743c7a Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 4 Dec 2019 17:46:28 -0800 Subject: [PATCH 058/662] fix(deps): pin TypeScript below 3.7.0 (#845) --- package.json | 2 +- renovate.json | 3 ++- src/auth/oauth2client.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 3f43bc25..03dfa5b8 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "source-map-support": "^0.5.6", "tmp": "^0.1.0", "ts-loader": "^6.0.0", - "typescript": "~3.7.0", + "typescript": "3.6.4", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" }, diff --git a/renovate.json b/renovate.json index 61f31b77..9518bf36 100644 --- a/renovate.json +++ b/renovate.json @@ -14,5 +14,6 @@ "extends": "packages:linters", "groupName": "linters" } - ] + ], + "ignoreDeps": ["typescript"] } diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 3a895c39..e2bc61f4 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -920,7 +920,7 @@ export class OAuth2Client extends AuthClient { try { const r = await this.getRequestMetadataAsync(opts.url); opts.headers = opts.headers || {}; - if (r.headers?.['x-goog-user-project']) { + if (r.headers && r.headers['x-goog-user-project']) { opts.headers['x-goog-user-project'] = r.headers['x-goog-user-project']; } if (r.headers && r.headers.Authorization) { From 660fd095c35c1b526be90708575ceb7d3ad698bf Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 4 Dec 2019 19:40:12 -0800 Subject: [PATCH 059/662] docs: adds new sample to samples/README.md (#842) --- samples/README.md | 18 ++++++++++++++++++ synth.metadata | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/samples/README.md b/samples/README.md index ca18ef7b..184ac832 100644 --- a/samples/README.md +++ b/samples/README.md @@ -20,6 +20,7 @@ * [Jwt](#jwt) * [Keepalive](#keepalive) * [Keyfile](#keyfile) + * [Oauth2-code Verifier](#oauth2-code-verifier) * [Oauth2](#oauth2) * [Verify Id Token](#verify-id-token) @@ -168,6 +169,23 @@ __Usage:__ +### Oauth2-code Verifier + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2-codeVerifier.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2-codeVerifier.js,samples/README.md) + +__Usage:__ + + +`node oauth2-codeVerifier.js` + + +----- + + + + ### Oauth2 View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2.js). diff --git a/synth.metadata b/synth.metadata index 75944fdc..11b5644b 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,5 +1,5 @@ { - "updateTime": "2019-11-14T12:07:29.320036Z", + "updateTime": "2019-12-03T12:07:47.595317Z", "sources": [ { "template": { From 758726588ad5cf8dc53c9c88784908c22077556c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:39:59 -0800 Subject: [PATCH 060/662] chore: release 5.6.1 (#843) --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f66c32bf..7d0cc497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.6.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.6.0...v5.6.1) (2019-12-05) + + +### Bug Fixes + +* **deps:** pin TypeScript below 3.7.0 ([#845](https://www.github.com/googleapis/google-auth-library-nodejs/issues/845)) ([a9c6e92](https://www.github.com/googleapis/google-auth-library-nodejs/commit/a9c6e9284efe8102974c57c9824ed6275d743c7a)) +* **docs:** improve types and docs for generateCodeVerifierAsync ([#840](https://www.github.com/googleapis/google-auth-library-nodejs/issues/840)) ([04dae9c](https://www.github.com/googleapis/google-auth-library-nodejs/commit/04dae9c271f0099025188489c61fd245d482832b)) + ## [5.6.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.5.1...v5.6.0) (2019-12-02) diff --git a/package.json b/package.json index 03dfa5b8..3a65a351 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.6.0", + "version": "5.6.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index f0508e72..3ccf4e9a 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.6.0", + "google-auth-library": "^5.6.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 46af865172103c6f28712d78b30c2291487cbe86 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 10 Dec 2019 15:01:25 -0500 Subject: [PATCH 061/662] feat: make x-goog-user-project work for additional auth clients (#848) --- src/auth/authclient.ts | 23 +++++++++++++++++++ src/auth/jwtclient.ts | 8 +++++-- src/auth/oauth2client.ts | 14 +---------- test/fixtures/service-account-with-quota.json | 12 ++++++++++ test/test.googleauth.ts | 2 ++ test/test.jwt.ts | 21 ++++++++++++++++- 6 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 test/fixtures/service-account-with-quota.json diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index ab10eac4..f7d4d13c 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -17,12 +17,14 @@ import {GaxiosOptions, GaxiosPromise} from 'gaxios'; import {DefaultTransporter} from '../transporters'; import {Credentials} from './credentials'; +import {Headers} from './oauth2client'; export declare interface AuthClient { on(event: 'tokens', listener: (tokens: Credentials) => void): this; } export abstract class AuthClient extends EventEmitter { + protected quotaProjectId?: string; transporter = new DefaultTransporter(); credentials: Credentials = {}; @@ -37,4 +39,25 @@ export abstract class AuthClient extends EventEmitter { setCredentials(credentials: Credentials) { this.credentials = credentials; } + + /** + * Append additional headers, e.g., x-goog-user-project, shared across the + * classes inheriting AuthClient. This method should be used by any method + * that overrides getRequestMetadataAsync(), which is a shared helper for + * setting request information in both gRPC and HTTP API calls. + * + * @param headers objedcdt to append additional headers to. + */ + protected addSharedMetadataHeaders(headers: Headers): Headers { + // quota_project_id, stored in application_default_credentials.json, is set in + // the x-goog-user-project header, to indicate an alternate account for + // billing and quota: + if ( + !headers['x-goog-user-project'] && // don't override a value the user sets. + this.quotaProjectId + ) { + headers['x-goog-user-project'] = this.quotaProjectId; + } + return headers; + } } diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index a2153a27..e5ffa58d 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -128,7 +128,11 @@ export class JWT extends OAuth2Client { }).target_audience ) { const {tokens} = await this.refreshToken(); - return {headers: {Authorization: `Bearer ${tokens.id_token}`}}; + return { + headers: this.addSharedMetadataHeaders({ + Authorization: `Bearer ${tokens.id_token}`, + }), + }; } else { // no scopes have been set, but a uri has been provided. Use JWTAccess // credentials. @@ -139,7 +143,7 @@ export class JWT extends OAuth2Client { url, this.additionalClaims ); - return {headers}; + return {headers: this.addSharedMetadataHeaders(headers)}; } } else { return super.getRequestMetadataAsync(url); diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index e2bc61f4..5dae743f 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -383,7 +383,6 @@ export class OAuth2Client extends AuthClient { private certificateCache: Certificates = {}; private certificateExpiry: Date | null = null; private certificateCacheFormat: CertificateFormat = CertificateFormat.PEM; - protected quotaProjectId?: string; protected refreshTokenPromises = new Map>(); // TODO: refactor tests to make this private @@ -808,18 +807,7 @@ export class OAuth2Client extends AuthClient { const headers: {[index: string]: string} = { Authorization: credentials.token_type + ' ' + tokens.access_token, }; - - // quota_project_id, stored in application_default_credentials.json, is set in - // the x-goog-user-project header, to indicate an alternate account for - // billing and quota: - if ( - !headers['x-goog-user-project'] && // don't override a value the user sets. - this.quotaProjectId - ) { - headers['x-goog-user-project'] = this.quotaProjectId; - } - - return {headers, res: r.res}; + return {headers: this.addSharedMetadataHeaders(headers), res: r.res}; } /** diff --git a/test/fixtures/service-account-with-quota.json b/test/fixtures/service-account-with-quota.json new file mode 100644 index 00000000..94ab5860 --- /dev/null +++ b/test/fixtures/service-account-with-quota.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "my-account", + "private_key_id": "abc123", + "client_email": "fake@example.com", + "client_id": "222222222222222222222", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/fake@example.com", + "quota_project_id": "fake-quota-project" + } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 4ae37080..8987026f 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1469,6 +1469,7 @@ describe('googleauth', () => { ); const auth = new GoogleAuth(); const client = await auth.getClient(); + assert(client instanceof UserRefreshClient); const headers = await client.getRequestHeaders(); assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); tokenReq.done(); @@ -1480,6 +1481,7 @@ describe('googleauth', () => { ); const auth = new GoogleAuth(); const client = await auth.getClient(); + assert(client instanceof UserRefreshClient); const apiReq = nock(BASE_URL) .post(ENDPOINT) .reply(function(uri) { diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 838f3de6..e3aecbc9 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -18,7 +18,7 @@ import * as jws from 'jws'; import * as nock from 'nock'; import * as sinon from 'sinon'; -import {JWT} from '../src'; +import {GoogleAuth, JWT} from '../src'; import {CredentialRequest, JWTInput} from '../src/auth/credentials'; const keypair = require('keypair'); @@ -810,3 +810,22 @@ it('getCredentials should handle a json keyFile', async () => { assert.strictEqual(private_key, json.private_key); assert.strictEqual(client_email, json.client_email); }); + +it('getRequestHeaders populates x-goog-user-project for JWT client', async () => { + const auth = new GoogleAuth({ + credentials: Object.assign( + require('../../test/fixtures/service-account-with-quota.json'), + { + private_key: keypair(1024 /* bitsize of private key */).private, + } + ), + }); + const client = await auth.getClient(); + assert(client instanceof JWT); + // If a URL isn't provided to authorize, the OAuth2Client super class is + // executed, which was already exercised. + const headers = await client.getRequestHeaders( + 'http:/example.com/my_test_service' + ); + assert.strictEqual(headers['x-goog-user-project'], 'fake-quota-project'); +}); From 1147733e3a33d9815cd8d3e271050ba8b277f5e1 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2019 12:22:00 -0800 Subject: [PATCH 062/662] chore: release 5.7.0 (#849) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0cc497..3a67d32f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.7.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.6.1...v5.7.0) (2019-12-10) + + +### Features + +* make x-goog-user-project work for additional auth clients ([#848](https://www.github.com/googleapis/google-auth-library-nodejs/issues/848)) ([46af865](https://www.github.com/googleapis/google-auth-library-nodejs/commit/46af865172103c6f28712d78b30c2291487cbe86)) + ### [5.6.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.6.0...v5.6.1) (2019-12-05) diff --git a/package.json b/package.json index 3a65a351..3795af98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.6.1", + "version": "5.7.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 3ccf4e9a..01fc9d31 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.6.1", + "google-auth-library": "^5.7.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From d4545a9001184fac0b67e7073e463e3efd345037 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 23 Dec 2019 16:34:22 -0500 Subject: [PATCH 063/662] feat: cache results of getEnv() (#857) --- package.json | 2 +- src/auth/envDetect.ts | 35 +++++++++++++++++++++-------------- test/test.googleauth.ts | 22 ++++++++++++++++++++++ 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 3795af98..6ef5543f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "base64-js": "^1.3.0", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", - "gcp-metadata": "^3.2.0", + "gcp-metadata": "^3.3.0", "gtoken": "^4.1.0", "jws": "^3.1.5", "lru-cache": "^5.0.0" diff --git a/src/auth/envDetect.ts b/src/auth/envDetect.ts index b5c908d5..2d78842f 100644 --- a/src/auth/envDetect.ts +++ b/src/auth/envDetect.ts @@ -22,27 +22,34 @@ export enum GCPEnv { NONE = 'NONE', } -let env: GCPEnv | undefined; +let envPromise: Promise | undefined; export function clear() { - env = undefined; + envPromise = undefined; } export async function getEnv() { - if (!env) { - if (isAppEngine()) { - env = GCPEnv.APP_ENGINE; - } else if (isCloudFunction()) { - env = GCPEnv.CLOUD_FUNCTIONS; - } else if (await isComputeEngine()) { - if (await isKubernetesEngine()) { - env = GCPEnv.KUBERNETES_ENGINE; - } else { - env = GCPEnv.COMPUTE_ENGINE; - } + if (envPromise) { + return envPromise; + } + envPromise = getEnvMemoized(); + return envPromise; +} + +async function getEnvMemoized(): Promise { + let env = GCPEnv.NONE; + if (isAppEngine()) { + env = GCPEnv.APP_ENGINE; + } else if (isCloudFunction()) { + env = GCPEnv.CLOUD_FUNCTIONS; + } else if (await isComputeEngine()) { + if (await isKubernetesEngine()) { + env = GCPEnv.KUBERNETES_ENGINE; } else { - env = GCPEnv.NONE; + env = GCPEnv.COMPUTE_ENGINE; } + } else { + env = GCPEnv.NONE; } return env; } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 8987026f..132c0fe5 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -22,6 +22,7 @@ import { HEADERS, HOST_ADDRESS, SECONDARY_HOST_ADDRESS, + resetIsAvailableCache, } from 'gcp-metadata'; import * as nock from 'nock'; import * as os from 'os'; @@ -80,6 +81,7 @@ describe('googleauth', () => { let createLinuxWellKnownStream: Function; let createWindowsWellKnownStream: Function; beforeEach(() => { + resetIsAvailableCache(); auth = new GoogleAuth(); exposeWindowsWellKnownFile = false; exposeLinuxWellKnownFile = false; @@ -1294,6 +1296,26 @@ describe('googleauth', () => { scope.done(); }); + it('should cache prior call to getEnv(), when GCE', async () => { + envDetect.clear(); + const {auth, scopes} = mockGCE(); + auth.getEnv(); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.COMPUTE_ENGINE); + }); + + it('should cache prior call to getEnv(), when GKE', async () => { + envDetect.clear(); + const {auth, scopes} = mockGCE(); + const scope = nock(host) + .get(`${instancePath}/attributes/cluster-name`) + .reply(200, {}, HEADERS); + auth.getEnv(); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.KUBERNETES_ENGINE); + scope.done(); + }); + it('should get the current environment if GCF 8 and below', async () => { envDetect.clear(); mockEnvVar('FUNCTION_NAME', 'DOGGY'); From e4a966779aefac6ad7e8a548d075c72148fb750b Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 23 Dec 2019 16:41:11 -0500 Subject: [PATCH 064/662] docs: update jsdoc license/samples-README (#853) --- .jsdoc.js | 29 ++++++++++++++--------------- samples/README.md | 30 ++++++++++++++++++------------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.jsdoc.js b/.jsdoc.js index cea5c2e0..bee5a0e5 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -1,18 +1,17 @@ -/*! - * Copyright 2018 Google LLC. All Rights Reserved. - * - * 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. - */ +// 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 +// +// https://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. +// 'use strict'; diff --git a/samples/README.md b/samples/README.md index 184ac832..b9d60ca2 100644 --- a/samples/README.md +++ b/samples/README.md @@ -29,6 +29,12 @@ Before running the samples, make sure you've followed the steps outlined in [Using the client library](https://github.com/googleapis/google-auth-library-nodejs#using-the-client-library). +`cd samples` + +`npm install` + +`cd ..` + ## Samples @@ -42,7 +48,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node adc.js` +`node samples/adc.js` ----- @@ -59,7 +65,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node compute.js` +`node samples/compute.js` ----- @@ -76,7 +82,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node credentials.js` +`node samples/credentials.js` ----- @@ -93,7 +99,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node headers.js` +`node samples/headers.js` ----- @@ -110,7 +116,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node iap.js` +`node samples/iap.js` ----- @@ -127,7 +133,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node jwt.js` +`node samples/jwt.js` ----- @@ -144,7 +150,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node keepalive.js` +`node samples/keepalive.js` ----- @@ -161,7 +167,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node keyfile.js` +`node samples/keyfile.js` ----- @@ -178,7 +184,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node oauth2-codeVerifier.js` +`node samples/oauth2-codeVerifier.js` ----- @@ -195,7 +201,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node oauth2.js` +`node samples/oauth2.js` ----- @@ -212,7 +218,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node verifyIdToken.js` +`node samples/verifyIdToken.js` @@ -221,4 +227,4 @@ __Usage:__ [shell_img]: https://gstatic.com/cloudssh/images/open-btn.png [shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/README.md -[product-docs]: https://cloud.google.com/docs/authentication/ \ No newline at end of file +[product-docs]: https://cloud.google.com/docs/authentication/ From 5b91fcccbfa818a7d5f131e3ca6eee09a9c1d4e9 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 24 Dec 2019 03:24:26 +0200 Subject: [PATCH 065/662] chore(deps): update dependency sinon to v8 (#856) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ef5543f..1cf2367f 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "nyc": "^14.1.1", "prettier": "^1.13.4", "puppeteer": "^2.0.0", - "sinon": "^7.0.0", + "sinon": "^8.0.0", "source-map-support": "^0.5.6", "tmp": "^0.1.0", "ts-loader": "^6.0.0", From 71366d43406047ce9e1d818d59a14191fb678e3a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 26 Dec 2019 23:44:02 +0200 Subject: [PATCH 066/662] fix(deps): update dependency jws to v4 (#851) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cf2367f..100f1b2a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "gaxios": "^2.1.0", "gcp-metadata": "^3.3.0", "gtoken": "^4.1.0", - "jws": "^3.1.5", + "jws": "^4.0.0", "lru-cache": "^5.0.0" }, "devDependencies": { From 34390781e26a4973be6dde4684cf5e5d8dade441 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 27 Dec 2019 11:32:30 -0800 Subject: [PATCH 067/662] build: use c8 for coverage (#860) --- package.json | 4 ++-- test/mocha.opts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 100f1b2a..7f439782 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "ncp": "^2.0.0", "nock": "^11.3.2", "null-loader": "^3.0.0", - "nyc": "^14.1.1", + "c8": "^7.0.0", "prettier": "^1.13.4", "puppeteer": "^2.0.0", "sinon": "^8.0.0", @@ -80,7 +80,7 @@ "!build/src/**/*.map" ], "scripts": { - "test": "nyc mocha build/test", + "test": "c8 mocha build/test", "clean": "gts clean", "prepare": "npm run compile", "lint": "gts check && eslint '**/*.js' && jsgl --local .", diff --git a/test/mocha.opts b/test/mocha.opts index fe40b757..49ca7744 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,3 @@ --require source-map-support/register ---timeout 10000 +--timeout 30000 --throw-deprecation From d4842198dc57bd08e244e518ee347b8f36501270 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 30 Dec 2019 11:35:56 -0800 Subject: [PATCH 068/662] refactor: use explicit mocha imports --- samples/test/.eslintrc.yml | 3 --- samples/test/jwt.test.js | 1 + system-test/test.kitchen.ts | 1 + test/test.compute.ts | 1 + test/test.googleauth.ts | 1 + test/test.iam.ts | 1 + test/test.index.ts | 1 + test/test.jwt.ts | 1 + test/test.jwtaccess.ts | 1 + test/test.loginticket.ts | 1 + test/test.oauth2.ts | 1 + test/test.refresh.ts | 1 + test/test.transporters.ts | 1 + 13 files changed, 12 insertions(+), 3 deletions(-) delete mode 100644 samples/test/.eslintrc.yml diff --git a/samples/test/.eslintrc.yml b/samples/test/.eslintrc.yml deleted file mode 100644 index 6db2a46c..00000000 --- a/samples/test/.eslintrc.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -env: - mocha: true diff --git a/samples/test/jwt.test.js b/samples/test/jwt.test.js index 5e613682..4da5c069 100644 --- a/samples/test/jwt.test.js +++ b/samples/test/jwt.test.js @@ -14,6 +14,7 @@ const cp = require('child_process'); const {assert} = require('chai'); +const {describe, it} = require('mocha'); const fs = require('fs'); const {promisify} = require('util'); diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 3ef001f3..30933d24 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; import * as execa from 'execa'; import * as fs from 'fs'; import * as mv from 'mv'; diff --git a/test/test.compute.ts b/test/test.compute.ts index 3650d6be..eee4dd01 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; const assertRejects = require('assert-rejects'); import {BASE_PATH, HEADERS, HOST_ADDRESS} from 'gcp-metadata'; import * as nock from 'nock'; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 132c0fe5..af01ed30 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; const assertRejects = require('assert-rejects'); import * as child_process from 'child_process'; import * as crypto from 'crypto'; diff --git a/test/test.iam.ts b/test/test.iam.ts index c09d5fb2..f86f38d0 100644 --- a/test/test.iam.ts +++ b/test/test.iam.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; import * as sinon from 'sinon'; import {IAMAuth} from '../src'; diff --git a/test/test.index.ts b/test/test.index.ts index 28296419..00d197ad 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; import * as gal from '../src'; it('should publicly export GoogleAuth', () => { diff --git a/test/test.jwt.ts b/test/test.jwt.ts index e3aecbc9..1f83ff53 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; import * as fs from 'fs'; import * as jws from 'jws'; import * as nock from 'nock'; diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index 80aa5fc9..e7728e5c 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; import * as fs from 'fs'; import * as jws from 'jws'; import * as sinon from 'sinon'; diff --git a/test/test.loginticket.ts b/test/test.loginticket.ts index fc56f4dd..757886a4 100644 --- a/test/test.loginticket.ts +++ b/test/test.loginticket.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; import {LoginTicket} from '../src/auth/loginticket'; it('should return null userId even if no payload', () => { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index aa84c7bc..81d8db23 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; const assertRejects = require('assert-rejects'); import * as crypto from 'crypto'; import * as fs from 'fs'; diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 9a5fcb1c..81cc3917 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; import * as fs from 'fs'; import * as nock from 'nock'; import {UserRefreshClient} from '../src'; diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 0f779afd..3d8ff52e 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -13,6 +13,7 @@ // limitations under the License. import * as assert from 'assert'; +import {describe, it} from 'mocha'; import {GaxiosOptions} from 'gaxios'; const assertRejects = require('assert-rejects'); import * as nock from 'nock'; From 73aa95d852cfa54ce44fcc8777bf54ea512e98a2 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 31 Dec 2019 02:34:08 +0200 Subject: [PATCH 069/662] chore(deps): update dependency execa to v4 (#852) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f439782..65267c7b 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "eslint-config-prettier": "^6.0.0", "eslint-plugin-node": "^10.0.0", "eslint-plugin-prettier": "^3.0.0", - "execa": "^3.0.0", + "execa": "^4.0.0", "gts": "^1.1.2", "is-docker": "^2.0.0", "js-green-licenses": "^1.0.0", From b277b23a962f22cf593b4cc8655cacb8c93da60b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 31 Dec 2019 03:05:06 +0200 Subject: [PATCH 070/662] chore(deps): update dependency eslint-plugin-node to v11 (#861) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65267c7b..266f97a0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "codecov": "^3.0.2", "eslint": "^6.0.0", "eslint-config-prettier": "^6.0.0", - "eslint-plugin-node": "^10.0.0", + "eslint-plugin-node": "^11.0.0", "eslint-plugin-prettier": "^3.0.0", "execa": "^4.0.0", "gts": "^1.1.2", From 05b4d3a15a4c2bf4e2c67581288aded1ff19730b Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 3 Jan 2020 18:44:06 -0800 Subject: [PATCH 071/662] chore: update .nycrc --- .nycrc | 1 + synth.metadata | 502 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 502 insertions(+), 1 deletion(-) diff --git a/.nycrc b/.nycrc index 36768884..b18d5472 100644 --- a/.nycrc +++ b/.nycrc @@ -12,6 +12,7 @@ "**/scripts", "**/protos", "**/test", + "**/*.d.ts", ".jsdoc.js", "**/.jsdoc.js", "karma.conf.js", diff --git a/synth.metadata b/synth.metadata index 11b5644b..b191ce9e 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,5 +1,5 @@ { - "updateTime": "2019-12-03T12:07:47.595317Z", + "updateTime": "2020-01-03T12:07:18.313096Z", "sources": [ { "template": { @@ -8,5 +8,505 @@ "version": "2019.10.17" } } + ], + "newFiles": [ + { + "path": "synth.metadata" + }, + { + "path": ".repo-metadata.json" + }, + { + "path": "webpack-tests.config.js" + }, + { + "path": "CONTRIBUTING.md" + }, + { + "path": "linkinator.config.json" + }, + { + "path": ".compodocrc" + }, + { + "path": ".prettierignore" + }, + { + "path": "tsconfig.json" + }, + { + "path": ".jsdoc.js" + }, + { + "path": ".gitignore" + }, + { + "path": "synth.py" + }, + { + "path": "CODE_OF_CONDUCT.md" + }, + { + "path": "README.md" + }, + { + "path": ".prettierrc" + }, + { + "path": "codecov.yaml" + }, + { + "path": ".nycrc" + }, + { + "path": "package.json" + }, + { + "path": "webpack.config.js" + }, + { + "path": ".eslintrc.yml" + }, + { + "path": "renovate.json" + }, + { + "path": "LICENSE" + }, + { + "path": "karma.conf.js" + }, + { + "path": "CHANGELOG.md" + }, + { + "path": ".eslintignore" + }, + { + "path": ".github/PULL_REQUEST_TEMPLATE.md" + }, + { + "path": ".github/release-please.yml" + }, + { + "path": ".github/ISSUE_TEMPLATE/support_request.md" + }, + { + "path": ".github/ISSUE_TEMPLATE/bug_report.md" + }, + { + "path": ".github/ISSUE_TEMPLATE/feature_request.md" + }, + { + "path": ".kokoro/samples-test.sh" + }, + { + "path": ".kokoro/system-test.sh" + }, + { + "path": ".kokoro/docs.sh" + }, + { + "path": ".kokoro/lint.sh" + }, + { + "path": ".kokoro/.gitattributes" + }, + { + "path": ".kokoro/publish.sh" + }, + { + "path": ".kokoro/trampoline.sh" + }, + { + "path": ".kokoro/common.cfg" + }, + { + "path": ".kokoro/test.bat" + }, + { + "path": ".kokoro/test.sh" + }, + { + "path": ".kokoro/browser-test.sh" + }, + { + "path": ".kokoro/release/docs.sh" + }, + { + "path": ".kokoro/release/docs.cfg" + }, + { + "path": ".kokoro/release/common.cfg" + }, + { + "path": ".kokoro/release/publish.cfg" + }, + { + "path": ".kokoro/presubmit/node12/test.cfg" + }, + { + "path": ".kokoro/presubmit/node12/common.cfg" + }, + { + "path": ".kokoro/presubmit/node8/browser-test.cfg" + }, + { + "path": ".kokoro/presubmit/node8/test.cfg" + }, + { + "path": ".kokoro/presubmit/node8/common.cfg" + }, + { + "path": ".kokoro/presubmit/windows/test.cfg" + }, + { + "path": ".kokoro/presubmit/windows/common.cfg" + }, + { + "path": ".kokoro/presubmit/node10/lint.cfg" + }, + { + "path": ".kokoro/presubmit/node10/system-test.cfg" + }, + { + "path": ".kokoro/presubmit/node10/test.cfg" + }, + { + "path": ".kokoro/presubmit/node10/docs.cfg" + }, + { + "path": ".kokoro/presubmit/node10/common.cfg" + }, + { + "path": ".kokoro/presubmit/node10/samples-test.cfg" + }, + { + "path": ".kokoro/continuous/node12/test.cfg" + }, + { + "path": ".kokoro/continuous/node12/common.cfg" + }, + { + "path": ".kokoro/continuous/node8/browser-test.cfg" + }, + { + "path": ".kokoro/continuous/node8/test.cfg" + }, + { + "path": ".kokoro/continuous/node8/common.cfg" + }, + { + "path": ".kokoro/continuous/node10/lint.cfg" + }, + { + "path": ".kokoro/continuous/node10/system-test.cfg" + }, + { + "path": ".kokoro/continuous/node10/test.cfg" + }, + { + "path": ".kokoro/continuous/node10/docs.cfg" + }, + { + "path": ".kokoro/continuous/node10/common.cfg" + }, + { + "path": ".kokoro/continuous/node10/samples-test.cfg" + }, + { + "path": "test/test.iam.ts" + }, + { + "path": "test/test.refresh.ts" + }, + { + "path": "test/test.crypto.ts" + }, + { + "path": "test/test.transporters.ts" + }, + { + "path": "test/mocha.opts" + }, + { + "path": "test/test.jwtaccess.ts" + }, + { + "path": "test/test.loginticket.ts" + }, + { + "path": "test/test.jwt.ts" + }, + { + "path": "test/test.compute.ts" + }, + { + "path": "test/test.oauth2.ts" + }, + { + "path": "test/test.index.ts" + }, + { + "path": "test/test.googleauth.ts" + }, + { + "path": "test/fixtures/private.pem" + }, + { + "path": "test/fixtures/private2.json" + }, + { + "path": "test/fixtures/empty.json" + }, + { + "path": "test/fixtures/oauthcertspem.json" + }, + { + "path": "test/fixtures/public.pem" + }, + { + "path": "test/fixtures/wellKnown.json" + }, + { + "path": "test/fixtures/refresh.json" + }, + { + "path": "test/fixtures/emptylink" + }, + { + "path": "test/fixtures/private.json" + }, + { + "path": "test/fixtures/service-account-with-quota.json" + }, + { + "path": "test/fixtures/goodlink" + }, + { + "path": "test/fixtures/key.p12" + }, + { + "path": "test/fixtures/config-no-quota/.config/gcloud/application_default_credentials.json" + }, + { + "path": "test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json" + }, + { + "path": "system-test/test.kitchen.ts" + }, + { + "path": "system-test/fixtures/kitchen/tsconfig.json" + }, + { + "path": "system-test/fixtures/kitchen/package.json" + }, + { + "path": "system-test/fixtures/kitchen/webpack.config.js" + }, + { + "path": "system-test/fixtures/kitchen/src/index.ts" + }, + { + "path": ".git/shallow" + }, + { + "path": ".git/HEAD" + }, + { + "path": ".git/config" + }, + { + "path": ".git/packed-refs" + }, + { + "path": ".git/index" + }, + { + "path": ".git/description" + }, + { + "path": ".git/objects/pack/pack-b534a6ad3ce20c8f198fd99151df4d9a9c3f049c.pack" + }, + { + "path": ".git/objects/pack/pack-b534a6ad3ce20c8f198fd99151df4d9a9c3f049c.idx" + }, + { + "path": ".git/logs/HEAD" + }, + { + "path": ".git/logs/refs/heads/master" + }, + { + "path": ".git/logs/refs/heads/autosynth" + }, + { + "path": ".git/logs/refs/remotes/origin/HEAD" + }, + { + "path": ".git/hooks/update.sample" + }, + { + "path": ".git/hooks/pre-push.sample" + }, + { + "path": ".git/hooks/pre-rebase.sample" + }, + { + "path": ".git/hooks/pre-commit.sample" + }, + { + "path": ".git/hooks/applypatch-msg.sample" + }, + { + "path": ".git/hooks/post-update.sample" + }, + { + "path": ".git/hooks/pre-applypatch.sample" + }, + { + "path": ".git/hooks/prepare-commit-msg.sample" + }, + { + "path": ".git/hooks/commit-msg.sample" + }, + { + "path": ".git/hooks/pre-receive.sample" + }, + { + "path": ".git/hooks/fsmonitor-watchman.sample" + }, + { + "path": ".git/refs/heads/master" + }, + { + "path": ".git/refs/heads/autosynth" + }, + { + "path": ".git/refs/remotes/origin/HEAD" + }, + { + "path": ".git/info/exclude" + }, + { + "path": "browser-test/test.crypto.ts" + }, + { + "path": "browser-test/test.oauth2.ts" + }, + { + "path": "browser-test/fixtures/keys.ts" + }, + { + "path": "src/messages.ts" + }, + { + "path": "src/index.ts" + }, + { + "path": "src/options.ts" + }, + { + "path": "src/transporters.ts" + }, + { + "path": "src/crypto/crypto.ts" + }, + { + "path": "src/crypto/node/crypto.ts" + }, + { + "path": "src/crypto/browser/crypto.ts" + }, + { + "path": "src/auth/refreshclient.ts" + }, + { + "path": "src/auth/googleauth.ts" + }, + { + "path": "src/auth/iam.ts" + }, + { + "path": "src/auth/oauth2client.ts" + }, + { + "path": "src/auth/jwtclient.ts" + }, + { + "path": "src/auth/computeclient.ts" + }, + { + "path": "src/auth/jwtaccess.ts" + }, + { + "path": "src/auth/credentials.ts" + }, + { + "path": "src/auth/envDetect.ts" + }, + { + "path": "src/auth/loginticket.ts" + }, + { + "path": "src/auth/authclient.ts" + }, + { + "path": "samples/credentials.js" + }, + { + "path": "samples/keepalive.js" + }, + { + "path": "samples/oauth2-codeVerifier.js" + }, + { + "path": "samples/verifyIdToken.js" + }, + { + "path": "samples/keyfile.js" + }, + { + "path": "samples/README.md" + }, + { + "path": "samples/headers.js" + }, + { + "path": "samples/iap.js" + }, + { + "path": "samples/jwt.js" + }, + { + "path": "samples/oauth2.js" + }, + { + "path": "samples/adc.js" + }, + { + "path": "samples/package.json" + }, + { + "path": "samples/compute.js" + }, + { + "path": "samples/.eslintrc.yml" + }, + { + "path": "samples/test/jwt.test.js" + }, + { + "path": "samples/puppeteer/oauth2-test.js" + }, + { + "path": "samples/puppeteer/package.json" + }, + { + "path": "samples/puppeteer/.eslintrc.yml" + }, + { + "path": "__pycache__/synth.cpython-36.pyc" + } ] } \ No newline at end of file From 376a815e03833cf3efc2f1f24be30785191922c1 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 6 Jan 2020 18:50:02 +0200 Subject: [PATCH 072/662] chore(deps): update dependency mocha to v7 (#865) --- package.json | 2 +- samples/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 266f97a0..c06f9540 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "karma-webpack": "^4.0.0", "keypair": "^1.0.1", "linkinator": "^1.5.0", - "mocha": "^6.0.0", + "mocha": "^7.0.0", "mv": "^2.1.1", "ncp": "^2.0.0", "nock": "^11.3.2", diff --git a/samples/package.json b/samples/package.json index 01fc9d31..4b23d106 100644 --- a/samples/package.json +++ b/samples/package.json @@ -19,6 +19,6 @@ }, "devDependencies": { "chai": "^4.2.0", - "mocha": "^6.0.0" + "mocha": "^7.0.0" } } From 30f02377cbf2e41c3746f39eebbbe4bb16f31fd2 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 10:46:54 -0800 Subject: [PATCH 073/662] chore: release 5.8.0 * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json [ci skip] --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a67d32f..73916b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.8.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.7.0...v5.8.0) (2020-01-06) + + +### Features + +* cache results of getEnv() ([#857](https://www.github.com/googleapis/google-auth-library-nodejs/issues/857)) ([d4545a9](https://www.github.com/googleapis/google-auth-library-nodejs/commit/d4545a9001184fac0b67e7073e463e3efd345037)) + + +### Bug Fixes + +* **deps:** update dependency jws to v4 ([#851](https://www.github.com/googleapis/google-auth-library-nodejs/issues/851)) ([71366d4](https://www.github.com/googleapis/google-auth-library-nodejs/commit/71366d43406047ce9e1d818d59a14191fb678e3a)) + ## [5.7.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.6.1...v5.7.0) (2019-12-10) diff --git a/package.json b/package.json index c06f9540..7d78fdc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.7.0", + "version": "5.8.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 4b23d106..8000a612 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.7.0", + "google-auth-library": "^5.8.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 8036f1a51d1a103b08daf62c7ce372c9f68cd9d4 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 14 Jan 2020 13:34:06 -0800 Subject: [PATCH 074/662] feat: add methods for fetching and using id tokens (#867) --- README.md | 45 ++++++++++++++++++ package.json | 2 +- samples/README.md | 25 ++++++++-- samples/iap.js | 41 ----------------- samples/idtokens-cloudrun.js | 55 ++++++++++++++++++++++ samples/idtokens-iap.js | 50 ++++++++++++++++++++ samples/test/jwt.test.js | 21 +++++++++ src/auth/computeclient.ts | 23 ++++++++++ src/auth/googleauth.ts | 16 +++++++ src/auth/idtokenclient.ts | 82 +++++++++++++++++++++++++++++++++ src/auth/jwtclient.ts | 26 ++++++++++- src/index.ts | 1 + synth.metadata | 8 +++- test/test.compute.ts | 36 +++++++++++++++ test/test.googleauth.ts | 53 ++++++++++++++++++++- test/test.idtokenclient.ts | 89 ++++++++++++++++++++++++++++++++++++ test/test.jwt.ts | 30 ++++++++++++ 17 files changed, 554 insertions(+), 49 deletions(-) delete mode 100644 samples/iap.js create mode 100644 samples/idtokens-cloudrun.js create mode 100644 samples/idtokens-iap.js create mode 100644 src/auth/idtokenclient.ts create mode 100644 test/test.idtokenclient.ts diff --git a/README.md b/README.md index b1780507..7a5324f8 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,51 @@ async function main() { main().catch(console.error); ``` +## Working with ID Tokens +If your application is running behind Cloud Run, or using Cloud Identity-Aware +Proxy (IAP), you will need to fetch an ID token to access your application. For +this, use the method `getIdTokenClient` on the `GoogleAuth` client. + +For invoking Cloud Run services, your service account will need the +[`Cloud Run Invoker`](https://cloud.google.com/run/docs/authenticating/service-to-service) +IAM permission. + +``` js +// Make a request to a protected Cloud Run +const {GoogleAuth} = require('google-auth-library'); + +async function main() { + const url = 'https://cloud-run-url.com'; + const auth = new GoogleAuth(); + const client = auth.getIdTokenClient(url); + const res = await client.request({url}); + console.log(res.data); +} + +main().catch(console.error); +``` + +For invoking Cloud Identity-Aware Proxy, you will need to pass the Client ID +used when you set up your protected resource as the target audience. + +``` js +// Make a request to a protected Cloud Identity-Aware Proxy (IAP) resource +const {GoogleAuth} = require('google-auth-library'); + +async function main() + const targetAudience = 'iap-client-id'; + const url = 'https://iap-url.com'; + const auth = new GoogleAuth(); + const client = auth.getIdTokenClient(targetAudience); + const res = await client.request({url}); + console.log(res.data); +} + +main().catch(console.error); +``` + +See how to [secure your IAP app with signed headers](https://cloud.google.com/iap/docs/signed-headers-howto). + ## Questions/problems? * Ask your development related questions on [Stack Overflow][stackoverflow]. diff --git a/package.json b/package.json index 7d78fdc9..3b90f7bd 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@types/sinon": "^7.0.0", "@types/tmp": "^0.1.0", "assert-rejects": "^1.0.0", + "c8": "^7.0.0", "chai": "^4.2.0", "codecov": "^3.0.2", "eslint": "^6.0.0", @@ -64,7 +65,6 @@ "ncp": "^2.0.0", "nock": "^11.3.2", "null-loader": "^3.0.0", - "c8": "^7.0.0", "prettier": "^1.13.4", "puppeteer": "^2.0.0", "sinon": "^8.0.0", diff --git a/samples/README.md b/samples/README.md index b9d60ca2..7d6e5e84 100644 --- a/samples/README.md +++ b/samples/README.md @@ -107,16 +107,33 @@ __Usage:__ -### Iap +### ID Tokens with IAP -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/iap.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). -[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/iap.js,samples/README.md) +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) __Usage:__ -`node samples/iap.js` +`node samples/idtokens-iap.js` + + +----- + + + + +### ID Tokens with Cloud Run + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-cloudrun.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-cloudrun.js,samples/README.md) + +__Usage:__ + + +`node samples/idtokens-cloudrun.js` ----- diff --git a/samples/iap.js b/samples/iap.js deleted file mode 100644 index 26529b28..00000000 --- a/samples/iap.js +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2017, Google, Inc. -// 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. - -'use strict'; - -const {JWT} = require('google-auth-library'); - -/** - * The JWT authorization is ideal for performing server-to-server - * communication without asking for user consent. - * - * Suggested reading for Admin SDK users using service accounts: - * https://developers.google.com/admin-sdk/directory/v1/guides/delegation - **/ - -const keys = require('./jwt.keys.json'); -const oauth2Keys = require('./iap.keys.json'); - -async function main() { - const clientId = oauth2Keys.web.client_id; - const client = new JWT({ - email: keys.client_email, - key: keys.private_key, - additionalClaims: {target_audience: clientId}, - }); - const url = `https://iap-demo-dot-el-gato.appspot.com`; - const res = await client.request({url}); - console.log(res.data); -} - -main().catch(console.error); diff --git a/samples/idtokens-cloudrun.js b/samples/idtokens-cloudrun.js new file mode 100644 index 00000000..213f3567 --- /dev/null +++ b/samples/idtokens-cloudrun.js @@ -0,0 +1,55 @@ +// Copyright 2020 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. + +// sample-metadata: +// title: ID Tokens for Cloud Run +// description: Requests a Cloud Run URL with an ID Token. +// usage: node idtokens-cloudrun.js [] + +'use strict'; + +function main( + url = 'https://service-1234-uc.a.run.app', + targetAudience = null +) { + // [START google_auth_idtoken_cloudrun] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const url = 'https://YOUR_CLOUD_RUN_URL.run.app'; + const {GoogleAuth} = require('google-auth-library'); + const auth = new GoogleAuth(); + + async function request() { + if (!targetAudience) { + // Use the request URL hostname as the target audience for Cloud Run requests + const {URL} = require('url'); + targetAudience = new URL(url).origin; + } + console.info( + `request Cloud Run ${url} with target audience ${targetAudience}` + ); + const client = await auth.getIdTokenClient(targetAudience); + const res = await client.request({url}); + console.info(res.data); + } + + request().catch(err => { + console.error(err.message); + process.exitCode = 1; + }); + // [END google_auth_idtoken_cloudrun] +} + +const args = process.argv.slice(2); +main(...args); diff --git a/samples/idtokens-iap.js b/samples/idtokens-iap.js new file mode 100644 index 00000000..c5c6135a --- /dev/null +++ b/samples/idtokens-iap.js @@ -0,0 +1,50 @@ +// Copyright 2020 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. + +// sample-metadata: +// title: ID Tokens for Identity-Aware Proxy (IAP) +// description: Requests an IAP-protected resource with an ID Token. +// usage: node idtokens-iap.js + +'use strict'; + +function main( + url = 'https://some.iap.url', + targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com' +) { + // [START google_auth_idtoken_iap] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const url = 'https://some.iap.url'; + // const targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com'; + + const {GoogleAuth} = require('google-auth-library'); + const auth = new GoogleAuth(); + + async function request() { + console.info(`request IAP ${url} with target audience ${targetAudience}`); + const client = await auth.getIdTokenClient(targetAudience); + const res = await client.request({url}); + console.info(res.data); + } + + request().catch(err => { + console.error(err.message); + process.exitCode = 1; + }); + // [END google_auth_idtoken_iap] +} + +const args = process.argv.slice(2); +main(...args); diff --git a/samples/test/jwt.test.js b/samples/test/jwt.test.js index 4da5c069..3664ec1e 100644 --- a/samples/test/jwt.test.js +++ b/samples/test/jwt.test.js @@ -63,4 +63,25 @@ describe('samples', () => { assert.match(output, /Headers:/); assert.match(output, /DNS Info:/); }); + + it('should fetch ID token for Cloud Run', async () => { + // process.env.CLOUD_RUN_URL should be a cloud run container, protected with + // IAP, running gcr.io/cloudrun/hello: + const url = + process.env.CLOUD_RUN_URL || 'https://hello-rftcw63abq-uc.a.run.app'; + const output = execSync(`node idtokens-cloudrun ${url}`); + assert.match(output, /What's next?/); + }); + + it('should fetch ID token for IAP', async () => { + // process.env.CLOUD_RUN_URL should be a cloud run container, protected with + // IAP, running gcr.io/cloudrun/hello: + const url = + process.env.IAP_URL || 'https://nodejs-docs-samples-iap.appspot.com'; + const targetAudience = + process.env.IAP_CLIENT_ID || + '170454875485-fbn7jalc9214bb67lslv1pbvmnijrb20.apps.googleusercontent.com'; + const output = execSync(`node idtokens-iap ${url} ${targetAudience}`); + assert.match(output, /Hello, world/); + }); }); diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 1b97bef6..479c8b51 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -19,6 +19,7 @@ import * as gcpMetadata from 'gcp-metadata'; import * as messages from '../messages'; import {CredentialRequest, Credentials} from './credentials'; +import {IdTokenProvider} from './idtokenclient'; import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; export interface ComputeOptions extends RefreshOptions { @@ -101,6 +102,28 @@ export class Compute extends OAuth2Client { return {tokens, res: null}; } + /** + * Fetches an ID token. + * @param targetAudience the audience for the fetched ID token. + */ + async fetchIdToken(targetAudience: string): Promise { + const idTokenPath = + `service-accounts/${this.serviceAccountEmail}/identity` + + `?audience=${targetAudience}`; + let idToken: string; + try { + const instanceOptions: gcpMetadata.Options = { + property: idTokenPath, + }; + idToken = await gcpMetadata.instance(instanceOptions); + } catch (e) { + e.message = `Could not fetch ID token: ${e.message}`; + throw e; + } + + return idToken; + } + protected wrapError(e: GaxiosError) { const res = e.response; if (res && res.status) { diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 64a9ed9c..c6d28e49 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -26,6 +26,7 @@ import {DefaultTransporter, Transporter} from '../transporters'; import {Compute, ComputeOptions} from './computeclient'; import {CredentialBody, JWTInput} from './credentials'; +import {IdTokenClient, IdTokenProvider} from './idtokenclient'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; import { @@ -728,6 +729,21 @@ export class GoogleAuth { 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: string): Promise { + 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({targetAudience, idTokenProvider: client}); + } + /** * Automatically obtain application default credentials, and return * an access token for making requests. diff --git a/src/auth/idtokenclient.ts b/src/auth/idtokenclient.ts new file mode 100644 index 00000000..9d0d7af5 --- /dev/null +++ b/src/auth/idtokenclient.ts @@ -0,0 +1,82 @@ +// Copyright 2020 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. + +import {BodyResponseCallback} from '../transporters'; + +import {Credentials} from './credentials'; +import {Headers, OAuth2Client, RequestMetadataResponse} from './oauth2client'; + +export interface IdTokenOptions { + /** + * The client to make the request to fetch an ID token. + */ + idTokenProvider: IdTokenProvider; + /** + * The audience to use when requesting an ID token. + */ + targetAudience: string; +} + +export interface IdTokenProvider { + fetchIdToken: (targetAudience: string) => Promise; +} + +export class IdTokenClient extends OAuth2Client { + targetAudience: string; + idTokenProvider: IdTokenProvider; + + /** + * Google ID Token client + * + * Retrieve access token from the metadata server. + * See: https://developers.google.com/compute/docs/authentication + */ + constructor(options: IdTokenOptions) { + super(); + this.targetAudience = options.targetAudience; + this.idTokenProvider = options.idTokenProvider; + } + + protected async getRequestMetadataAsync( + url?: string | null + ): Promise { + if ( + !this.credentials.id_token || + (this.credentials.expiry_date || 0) < Date.now() + ) { + const idToken = await this.idTokenProvider.fetchIdToken( + this.targetAudience + ); + this.credentials = { + id_token: idToken, + expiry_date: this.getIdTokenExpiryDate(idToken), + } as Credentials; + } + + const headers: Headers = { + Authorization: 'Bearer ' + this.credentials.id_token, + }; + return {headers}; + } + + private getIdTokenExpiryDate(idToken: string): number | void { + const payloadB64 = idToken.split('.')[1]; + if (payloadB64) { + const payload = JSON.parse( + Buffer.from(payloadB64, 'base64').toString('ascii') + ); + return payload.exp * 1000; + } + } +} diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index e5ffa58d..e4f173dc 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -17,6 +17,7 @@ import * as stream from 'stream'; import * as messages from '../messages'; import {CredentialBody, Credentials, JWTInput} from './credentials'; +import {IdTokenProvider} from './idtokenclient'; import {JWTAccess} from './jwtaccess'; import { GetTokenResponse, @@ -35,7 +36,7 @@ export interface JWTOptions extends RefreshOptions { additionalClaims?: {}; } -export class JWT extends OAuth2Client { +export class JWT extends OAuth2Client implements IdTokenProvider { email?: string; keyFile?: string; key?: string; @@ -150,6 +151,29 @@ export class JWT extends OAuth2Client { } } + /** + * Fetches an ID token. + * @param targetAudience the audience for the fetched ID token. + */ + async fetchIdToken(targetAudience: string): Promise { + // Create a new gToken for fetching an ID token + const gtoken = new GoogleToken({ + iss: this.email, + sub: this.subject, + scope: this.scopes, + keyFile: this.keyFile, + key: this.key, + additionalClaims: {target_audience: targetAudience}, + }); + await gtoken.getToken({ + forceRefresh: true, + }); + if (!gtoken.idToken) { + throw new Error('Unknown error: Failed to fetch ID token'); + } + return gtoken.idToken; + } + /** * Indicates whether the credential requires scopes to be created by calling * createScoped before use. diff --git a/src/index.ts b/src/index.ts index 39d98b3b..1299a721 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ export { export {GCPEnv} from './auth/envDetect'; export {GoogleAuthOptions, ProjectIdCallback} from './auth/googleauth'; export {IAMAuth, RequestMetadata} from './auth/iam'; +export {IdTokenClient, IdTokenProvider} from './auth/idtokenclient'; export {Claims, JWTAccess} from './auth/jwtaccess'; export {JWT, JWTOptions} from './auth/jwtclient'; export { diff --git a/synth.metadata b/synth.metadata index b191ce9e..825a6721 100644 --- a/synth.metadata +++ b/synth.metadata @@ -427,6 +427,9 @@ { "path": "src/auth/iam.ts" }, + { + "path": "src/auth/idtokenclient.ts" + }, { "path": "src/auth/oauth2client.ts" }, @@ -473,7 +476,10 @@ "path": "samples/headers.js" }, { - "path": "samples/iap.js" + "path": "samples/idtokens-cloudrun.js" + }, + { + "path": "samples/idtokens-iap.js" }, { "path": "samples/jwt.js" diff --git a/test/test.compute.ts b/test/test.compute.ts index eee4dd01..74f9925c 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -24,6 +24,7 @@ nock.disableNetConnect(); const url = 'http://example.com'; const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`; +const identityPath = `${BASE_PATH}/instance/service-accounts/default/identity`; function mockToken(statusCode = 200, scopes?: string[]) { let path = tokenPath; if (scopes && scopes.length > 0) { @@ -231,3 +232,38 @@ it('should accept a custom service account', async () => { scopes.forEach(s => s.done()); assert.strictEqual(compute.credentials.access_token, 'abc123'); }); + +it('should request the identity endpoint for fetchIdToken', async () => { + const targetAudience = 'a-target-audience'; + const path = `${identityPath}?audience=${targetAudience}`; + + const tokenFetchNock = nock(HOST_ADDRESS) + .get(path, undefined, {reqheaders: HEADERS}) + .reply(200, 'abc123', HEADERS); + + const compute = new Compute(); + const idToken = await compute.fetchIdToken(targetAudience); + + tokenFetchNock.done(); + + assert.strictEqual(idToken, 'abc123'); +}); + +it('should throw an error if metadata server is unavailable', async () => { + const targetAudience = 'a-target-audience'; + const path = `${identityPath}?audience=${targetAudience}`; + + const tokenFetchNock = nock(HOST_ADDRESS) + .get(path, undefined, {reqheaders: HEADERS}) + .reply(500, 'a server error!', HEADERS); + + const compute = new Compute(); + try { + await compute.fetchIdToken(targetAudience); + } catch { + tokenFetchNock.done(); + return; + } + + assert.fail('failed to throw'); +}); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index af01ed30..5b5c2bb6 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -30,7 +30,7 @@ import * as os from 'os'; import * as path from 'path'; import * as sinon from 'sinon'; -import {GoogleAuth, JWT, UserRefreshClient} from '../src'; +import {GoogleAuth, JWT, UserRefreshClient, IdTokenClient} from '../src'; import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; import {Compute} from '../src/auth/computeclient'; @@ -1524,6 +1524,57 @@ describe('googleauth', () => { apiReq.done(); }); + it('should return a Compute client for getIdTokenClient', async () => { + const nockScopes = [nockIsGCE(), createGetProjectIdNock()]; + const auth = new GoogleAuth(); + const client = await auth.getIdTokenClient('a-target-audience'); + assert(client instanceof IdTokenClient); + assert(client.idTokenProvider instanceof Compute); + }); + + it('should return a JWT client for getIdTokenClient', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); + + const auth = new GoogleAuth(); + const client = await auth.getIdTokenClient('a-target-audience'); + assert(client instanceof IdTokenClient); + assert(client.idTokenProvider instanceof JWT); + }); + + it('should call getClient for getIdTokenClient', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); + + const spy = sinon.spy(auth, 'getClient'); + const client = await auth.getIdTokenClient('a-target-audience'); + assert(client instanceof IdTokenClient); + assert(spy.calledOnce); + }); + + it('should fail when using UserRefreshClient', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/refresh.json' + ); + mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id'); + + try { + const client = await auth.getIdTokenClient('a-target-audience'); + } catch (e) { + assert(e.message.startsWith('Cannot fetch ID token in this environment')); + return; + } + assert.fail('failed to throw'); + }); + function mockApplicationDefaultCredentials(path: string) { // Fake a home directory in our fixtures path. mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); diff --git a/test/test.idtokenclient.ts b/test/test.idtokenclient.ts new file mode 100644 index 00000000..bfe88a8f --- /dev/null +++ b/test/test.idtokenclient.ts @@ -0,0 +1,89 @@ +// Copyright 2020 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. +import * as assert from 'assert'; +import {it} from 'mocha'; +import * as fs from 'fs'; +import * as nock from 'nock'; + +import {IdTokenClient, JWT} from '../src'; +import {CredentialRequest} from '../src/auth/credentials'; + +const PEM_PATH = './test/fixtures/private.pem'; +const PEM_CONTENTS = fs.readFileSync(PEM_PATH, 'utf8'); +nock.disableNetConnect(); + +function createGTokenMock(body: CredentialRequest) { + return nock('https://www.googleapis.com') + .post('/oauth2/v4/token') + .reply(200, body); +} + +afterEach(() => { + nock.cleanAll(); +}); + +it('should determine expiry_date from JWT', async () => { + const idToken = 'header.eyJleHAiOiAxNTc4NzAyOTU2fQo.signature'; + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); + + const scope = createGTokenMock({id_token: idToken}); + const targetAudience = 'a-target-audience'; + const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); + await client.getRequestHeaders(); + scope.done(); + assert.strictEqual(client.credentials.expiry_date, 1578702956000); +}); + +it('should refresh ID token if expired', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); + + const scope = createGTokenMock({id_token: 'abc123'}); + const targetAudience = 'a-target-audience'; + const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); + client.credentials = { + id_token: 'an-identity-token', + expiry_date: new Date().getTime() - 1000, + }; + const headers = await client.getRequestHeaders(); + scope.done(); + assert.strictEqual(client.credentials.id_token, 'abc123'); + assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); +}); + +it('should refresh ID token if expiry_date not set', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); + + const scope = createGTokenMock({id_token: 'abc123'}); + const targetAudience = 'a-target-audience'; + const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); + client.credentials = { + id_token: 'an-identity-token', + }; + const headers = await client.getRequestHeaders(); + scope.done(); + assert.strictEqual(client.credentials.id_token, 'abc123'); + assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); +}); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 1f83ff53..12d00916 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -830,3 +830,33 @@ it('getRequestHeaders populates x-goog-user-project for JWT client', async () => ); assert.strictEqual(headers['x-goog-user-project'], 'fake-quota-project'); }); + +it('should return an ID token for fetchIdToken', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); + + const scope = createGTokenMock({id_token: 'abc123'}); + const idtoken = await jwt.fetchIdToken('a-target-audience'); + scope.done(); + assert.strictEqual(idtoken, 'abc123'); +}); + +it('should throw an error if ID token is not set', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); + + const scope = createGTokenMock({access_token: 'a-token'}); + try { + await jwt.fetchIdToken('a-target-audience'); + } catch { + scope.done(); + return; + } + assert.fail('failed to throw'); +}); From 539ea5e804386b79ecf469838fff19465aeb2ca6 Mon Sep 17 00:00:00 2001 From: "Herman J. Radtke III" Date: Tue, 14 Jan 2020 13:50:10 -0800 Subject: [PATCH 075/662] feat: export LoginTicket and TokenPayload (#870) Co-authored-by: Benjamin E. Coe --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 1299a721..722e14a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export { TokenInfo, VerifyIdTokenOptions, } from './auth/oauth2client'; +export {LoginTicket, TokenPayload} from './auth/loginticket'; export { UserRefreshClient, UserRefreshClientOptions, From 4eb3e78f77a7fe76127a944f2598e83a79845187 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 14 Jan 2020 14:08:51 -0800 Subject: [PATCH 076/662] docs: update samples readme with new samples (#873) Co-authored-by: Justin Beckwith --- samples/README.md | 23 ++- synth.metadata | 508 +--------------------------------------------- 2 files changed, 15 insertions(+), 516 deletions(-) diff --git a/samples/README.md b/samples/README.md index 7d6e5e84..2d740d81 100644 --- a/samples/README.md +++ b/samples/README.md @@ -16,7 +16,8 @@ * [Compute](#compute) * [Credentials](#credentials) * [Headers](#headers) - * [Iap](#iap) + * [ID Tokens for Cloud Run](#id-tokens-for-cloud-run) + * [ID Tokens for Identity-Aware Proxy (IAP)](#id-tokens-for-identity-aware-proxy-iap) * [Jwt](#jwt) * [Keepalive](#keepalive) * [Keyfile](#keyfile) @@ -107,16 +108,18 @@ __Usage:__ -### ID Tokens with IAP +### ID Tokens for Cloud Run -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). +Requests a Cloud Run URL with an ID Token. -[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-cloudrun.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-cloudrun.js,samples/README.md) __Usage:__ -`node samples/idtokens-iap.js` +`node idtokens-cloudrun.js []` ----- @@ -124,16 +127,18 @@ __Usage:__ -### ID Tokens with Cloud Run +### ID Tokens for Identity-Aware Proxy (IAP) -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-cloudrun.js). +Requests an IAP-protected resource with an ID Token. -[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-cloudrun.js,samples/README.md) +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) __Usage:__ -`node samples/idtokens-cloudrun.js` +`node idtokens-iap.js ` ----- diff --git a/synth.metadata b/synth.metadata index 825a6721..21bc02b3 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,5 +1,5 @@ { - "updateTime": "2020-01-03T12:07:18.313096Z", + "updateTime": "2020-01-14T21:35:49.564506Z", "sources": [ { "template": { @@ -8,511 +8,5 @@ "version": "2019.10.17" } } - ], - "newFiles": [ - { - "path": "synth.metadata" - }, - { - "path": ".repo-metadata.json" - }, - { - "path": "webpack-tests.config.js" - }, - { - "path": "CONTRIBUTING.md" - }, - { - "path": "linkinator.config.json" - }, - { - "path": ".compodocrc" - }, - { - "path": ".prettierignore" - }, - { - "path": "tsconfig.json" - }, - { - "path": ".jsdoc.js" - }, - { - "path": ".gitignore" - }, - { - "path": "synth.py" - }, - { - "path": "CODE_OF_CONDUCT.md" - }, - { - "path": "README.md" - }, - { - "path": ".prettierrc" - }, - { - "path": "codecov.yaml" - }, - { - "path": ".nycrc" - }, - { - "path": "package.json" - }, - { - "path": "webpack.config.js" - }, - { - "path": ".eslintrc.yml" - }, - { - "path": "renovate.json" - }, - { - "path": "LICENSE" - }, - { - "path": "karma.conf.js" - }, - { - "path": "CHANGELOG.md" - }, - { - "path": ".eslintignore" - }, - { - "path": ".github/PULL_REQUEST_TEMPLATE.md" - }, - { - "path": ".github/release-please.yml" - }, - { - "path": ".github/ISSUE_TEMPLATE/support_request.md" - }, - { - "path": ".github/ISSUE_TEMPLATE/bug_report.md" - }, - { - "path": ".github/ISSUE_TEMPLATE/feature_request.md" - }, - { - "path": ".kokoro/samples-test.sh" - }, - { - "path": ".kokoro/system-test.sh" - }, - { - "path": ".kokoro/docs.sh" - }, - { - "path": ".kokoro/lint.sh" - }, - { - "path": ".kokoro/.gitattributes" - }, - { - "path": ".kokoro/publish.sh" - }, - { - "path": ".kokoro/trampoline.sh" - }, - { - "path": ".kokoro/common.cfg" - }, - { - "path": ".kokoro/test.bat" - }, - { - "path": ".kokoro/test.sh" - }, - { - "path": ".kokoro/browser-test.sh" - }, - { - "path": ".kokoro/release/docs.sh" - }, - { - "path": ".kokoro/release/docs.cfg" - }, - { - "path": ".kokoro/release/common.cfg" - }, - { - "path": ".kokoro/release/publish.cfg" - }, - { - "path": ".kokoro/presubmit/node12/test.cfg" - }, - { - "path": ".kokoro/presubmit/node12/common.cfg" - }, - { - "path": ".kokoro/presubmit/node8/browser-test.cfg" - }, - { - "path": ".kokoro/presubmit/node8/test.cfg" - }, - { - "path": ".kokoro/presubmit/node8/common.cfg" - }, - { - "path": ".kokoro/presubmit/windows/test.cfg" - }, - { - "path": ".kokoro/presubmit/windows/common.cfg" - }, - { - "path": ".kokoro/presubmit/node10/lint.cfg" - }, - { - "path": ".kokoro/presubmit/node10/system-test.cfg" - }, - { - "path": ".kokoro/presubmit/node10/test.cfg" - }, - { - "path": ".kokoro/presubmit/node10/docs.cfg" - }, - { - "path": ".kokoro/presubmit/node10/common.cfg" - }, - { - "path": ".kokoro/presubmit/node10/samples-test.cfg" - }, - { - "path": ".kokoro/continuous/node12/test.cfg" - }, - { - "path": ".kokoro/continuous/node12/common.cfg" - }, - { - "path": ".kokoro/continuous/node8/browser-test.cfg" - }, - { - "path": ".kokoro/continuous/node8/test.cfg" - }, - { - "path": ".kokoro/continuous/node8/common.cfg" - }, - { - "path": ".kokoro/continuous/node10/lint.cfg" - }, - { - "path": ".kokoro/continuous/node10/system-test.cfg" - }, - { - "path": ".kokoro/continuous/node10/test.cfg" - }, - { - "path": ".kokoro/continuous/node10/docs.cfg" - }, - { - "path": ".kokoro/continuous/node10/common.cfg" - }, - { - "path": ".kokoro/continuous/node10/samples-test.cfg" - }, - { - "path": "test/test.iam.ts" - }, - { - "path": "test/test.refresh.ts" - }, - { - "path": "test/test.crypto.ts" - }, - { - "path": "test/test.transporters.ts" - }, - { - "path": "test/mocha.opts" - }, - { - "path": "test/test.jwtaccess.ts" - }, - { - "path": "test/test.loginticket.ts" - }, - { - "path": "test/test.jwt.ts" - }, - { - "path": "test/test.compute.ts" - }, - { - "path": "test/test.oauth2.ts" - }, - { - "path": "test/test.index.ts" - }, - { - "path": "test/test.googleauth.ts" - }, - { - "path": "test/fixtures/private.pem" - }, - { - "path": "test/fixtures/private2.json" - }, - { - "path": "test/fixtures/empty.json" - }, - { - "path": "test/fixtures/oauthcertspem.json" - }, - { - "path": "test/fixtures/public.pem" - }, - { - "path": "test/fixtures/wellKnown.json" - }, - { - "path": "test/fixtures/refresh.json" - }, - { - "path": "test/fixtures/emptylink" - }, - { - "path": "test/fixtures/private.json" - }, - { - "path": "test/fixtures/service-account-with-quota.json" - }, - { - "path": "test/fixtures/goodlink" - }, - { - "path": "test/fixtures/key.p12" - }, - { - "path": "test/fixtures/config-no-quota/.config/gcloud/application_default_credentials.json" - }, - { - "path": "test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json" - }, - { - "path": "system-test/test.kitchen.ts" - }, - { - "path": "system-test/fixtures/kitchen/tsconfig.json" - }, - { - "path": "system-test/fixtures/kitchen/package.json" - }, - { - "path": "system-test/fixtures/kitchen/webpack.config.js" - }, - { - "path": "system-test/fixtures/kitchen/src/index.ts" - }, - { - "path": ".git/shallow" - }, - { - "path": ".git/HEAD" - }, - { - "path": ".git/config" - }, - { - "path": ".git/packed-refs" - }, - { - "path": ".git/index" - }, - { - "path": ".git/description" - }, - { - "path": ".git/objects/pack/pack-b534a6ad3ce20c8f198fd99151df4d9a9c3f049c.pack" - }, - { - "path": ".git/objects/pack/pack-b534a6ad3ce20c8f198fd99151df4d9a9c3f049c.idx" - }, - { - "path": ".git/logs/HEAD" - }, - { - "path": ".git/logs/refs/heads/master" - }, - { - "path": ".git/logs/refs/heads/autosynth" - }, - { - "path": ".git/logs/refs/remotes/origin/HEAD" - }, - { - "path": ".git/hooks/update.sample" - }, - { - "path": ".git/hooks/pre-push.sample" - }, - { - "path": ".git/hooks/pre-rebase.sample" - }, - { - "path": ".git/hooks/pre-commit.sample" - }, - { - "path": ".git/hooks/applypatch-msg.sample" - }, - { - "path": ".git/hooks/post-update.sample" - }, - { - "path": ".git/hooks/pre-applypatch.sample" - }, - { - "path": ".git/hooks/prepare-commit-msg.sample" - }, - { - "path": ".git/hooks/commit-msg.sample" - }, - { - "path": ".git/hooks/pre-receive.sample" - }, - { - "path": ".git/hooks/fsmonitor-watchman.sample" - }, - { - "path": ".git/refs/heads/master" - }, - { - "path": ".git/refs/heads/autosynth" - }, - { - "path": ".git/refs/remotes/origin/HEAD" - }, - { - "path": ".git/info/exclude" - }, - { - "path": "browser-test/test.crypto.ts" - }, - { - "path": "browser-test/test.oauth2.ts" - }, - { - "path": "browser-test/fixtures/keys.ts" - }, - { - "path": "src/messages.ts" - }, - { - "path": "src/index.ts" - }, - { - "path": "src/options.ts" - }, - { - "path": "src/transporters.ts" - }, - { - "path": "src/crypto/crypto.ts" - }, - { - "path": "src/crypto/node/crypto.ts" - }, - { - "path": "src/crypto/browser/crypto.ts" - }, - { - "path": "src/auth/refreshclient.ts" - }, - { - "path": "src/auth/googleauth.ts" - }, - { - "path": "src/auth/iam.ts" - }, - { - "path": "src/auth/idtokenclient.ts" - }, - { - "path": "src/auth/oauth2client.ts" - }, - { - "path": "src/auth/jwtclient.ts" - }, - { - "path": "src/auth/computeclient.ts" - }, - { - "path": "src/auth/jwtaccess.ts" - }, - { - "path": "src/auth/credentials.ts" - }, - { - "path": "src/auth/envDetect.ts" - }, - { - "path": "src/auth/loginticket.ts" - }, - { - "path": "src/auth/authclient.ts" - }, - { - "path": "samples/credentials.js" - }, - { - "path": "samples/keepalive.js" - }, - { - "path": "samples/oauth2-codeVerifier.js" - }, - { - "path": "samples/verifyIdToken.js" - }, - { - "path": "samples/keyfile.js" - }, - { - "path": "samples/README.md" - }, - { - "path": "samples/headers.js" - }, - { - "path": "samples/idtokens-cloudrun.js" - }, - { - "path": "samples/idtokens-iap.js" - }, - { - "path": "samples/jwt.js" - }, - { - "path": "samples/oauth2.js" - }, - { - "path": "samples/adc.js" - }, - { - "path": "samples/package.json" - }, - { - "path": "samples/compute.js" - }, - { - "path": "samples/.eslintrc.yml" - }, - { - "path": "samples/test/jwt.test.js" - }, - { - "path": "samples/puppeteer/oauth2-test.js" - }, - { - "path": "samples/puppeteer/package.json" - }, - { - "path": "samples/puppeteer/.eslintrc.yml" - }, - { - "path": "__pycache__/synth.cpython-36.pyc" - } ] } \ No newline at end of file From 2053228204baee38d9cfe69fb4328b2efd2d5131 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2020 16:14:48 -0800 Subject: [PATCH 077/662] chore: release 5.9.0 (#872) --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73916b7b..d863da3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.9.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.8.0...v5.9.0) (2020-01-14) + + +### Features + +* add methods for fetching and using id tokens ([#867](https://www.github.com/googleapis/google-auth-library-nodejs/issues/867)) ([8036f1a](https://www.github.com/googleapis/google-auth-library-nodejs/commit/8036f1a51d1a103b08daf62c7ce372c9f68cd9d4)) +* export LoginTicket and TokenPayload ([#870](https://www.github.com/googleapis/google-auth-library-nodejs/issues/870)) ([539ea5e](https://www.github.com/googleapis/google-auth-library-nodejs/commit/539ea5e804386b79ecf469838fff19465aeb2ca6)) + ## [5.8.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.7.0...v5.8.0) (2020-01-06) diff --git a/package.json b/package.json index 3b90f7bd..460978d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.8.0", + "version": "5.9.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 8000a612..16b9806e 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.8.0", + "google-auth-library": "^5.9.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From e45b73dbb22e1c2d8115882006a21337c7d9bd63 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 16 Jan 2020 11:37:22 -0800 Subject: [PATCH 078/662] fix: ensures GCE metadata sets email field for ID tokens (#874) --- src/auth/computeclient.ts | 2 +- test/test.compute.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 479c8b51..57b83def 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -109,7 +109,7 @@ export class Compute extends OAuth2Client { async fetchIdToken(targetAudience: string): Promise { const idTokenPath = `service-accounts/${this.serviceAccountEmail}/identity` + - `?audience=${targetAudience}`; + `?format=full&audience=${targetAudience}`; let idToken: string; try { const instanceOptions: gcpMetadata.Options = { diff --git a/test/test.compute.ts b/test/test.compute.ts index 74f9925c..9e191452 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -235,7 +235,7 @@ it('should accept a custom service account', async () => { it('should request the identity endpoint for fetchIdToken', async () => { const targetAudience = 'a-target-audience'; - const path = `${identityPath}?audience=${targetAudience}`; + const path = `${identityPath}?format=full&audience=${targetAudience}`; const tokenFetchNock = nock(HOST_ADDRESS) .get(path, undefined, {reqheaders: HEADERS}) @@ -251,7 +251,7 @@ it('should request the identity endpoint for fetchIdToken', async () => { it('should throw an error if metadata server is unavailable', async () => { const targetAudience = 'a-target-audience'; - const path = `${identityPath}?audience=${targetAudience}`; + const path = `${identityPath}?format=full&audience=${targetAudience}`; const tokenFetchNock = nock(HOST_ADDRESS) .get(path, undefined, {reqheaders: HEADERS}) From c215e1bffb666ef91e4013258ad6c4edaa5f6041 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 16 Jan 2020 12:11:52 -0800 Subject: [PATCH 079/662] chore: release 5.9.1 (#875) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d863da3a..53d29a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.9.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.9.0...v5.9.1) (2020-01-16) + + +### Bug Fixes + +* ensures GCE metadata sets email field for ID tokens ([#874](https://www.github.com/googleapis/google-auth-library-nodejs/issues/874)) ([e45b73d](https://www.github.com/googleapis/google-auth-library-nodejs/commit/e45b73dbb22e1c2d8115882006a21337c7d9bd63)) + ## [5.9.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.8.0...v5.9.0) (2020-01-14) diff --git a/package.json b/package.json index 460978d9..8bb87354 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.9.0", + "version": "5.9.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 16b9806e..86175edb 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.9.0", + "google-auth-library": "^5.9.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 880b3c9cdcde775a25afbc8fb65108f3aeb0b65b Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 21 Jan 2020 15:38:44 -0800 Subject: [PATCH 080/662] build: update windows configuration to match new vm --- .kokoro/test.bat | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.kokoro/test.bat b/.kokoro/test.bat index fddff757..ae59e59b 100644 --- a/.kokoro/test.bat +++ b/.kokoro/test.bat @@ -17,14 +17,12 @@ cd /d %~dp0 cd .. -@rem The image we're currently running has a broken version of Node.js enabled -@rem by nvm (v10.15.3), which has no npm bin. This hack uses the functional -@rem Node v8.9.1 to install npm@latest, it then uses this version of npm to -@rem install npm for v10.15.3. -call nvm use v8.9.1 || goto :error -call node C:\Users\kbuilder\AppData\Roaming\nvm-ps\versions\v8.9.1\node_modules\npm-bootstrap\bin\npm-cli.js i npm -g || goto :error -call nvm use v10.15.3 || goto :error -call node C:\Users\kbuilder\AppData\Roaming\nvm-ps\versions\v8.9.1\node_modules\npm\bin\npm-cli.js i npm -g || goto :error +@rem npm path is not currently set in our image, we should fix this next time +@rem we upgrade Node.js in the image: +SET PATH=%PATH%;/cygdrive/c/Program Files/nodejs/npm + +call nvm use v12.14.1 +call which node call npm install || goto :error call npm run test || goto :error From 52be93e81b961355880fe0c355c0bc15a943ce51 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Thu, 23 Jan 2020 16:23:50 -0800 Subject: [PATCH 081/662] chore: clear synth.metadata --- synth.metadata | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 synth.metadata diff --git a/synth.metadata b/synth.metadata deleted file mode 100644 index 21bc02b3..00000000 --- a/synth.metadata +++ /dev/null @@ -1,12 +0,0 @@ -{ - "updateTime": "2020-01-14T21:35:49.564506Z", - "sources": [ - { - "template": { - "name": "node_library", - "origin": "synthtool.gcp", - "version": "2019.10.17" - } - } - ] -} \ No newline at end of file From c74c47078407bf2f867449bb9a59d705b1381f05 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 24 Jan 2020 10:53:55 -0800 Subject: [PATCH 082/662] chore: regenerate synth.metadata (#880) --- synth.metadata | 434 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 synth.metadata diff --git a/synth.metadata b/synth.metadata new file mode 100644 index 00000000..4dead686 --- /dev/null +++ b/synth.metadata @@ -0,0 +1,434 @@ +{ + "updateTime": "2020-01-24T12:08:38.705266Z", + "sources": [ + { + "template": { + "name": "node_library", + "origin": "synthtool.gcp", + "version": "2019.10.17" + } + } + ], + "newFiles": [ + { + "path": ".compodocrc" + }, + { + "path": ".eslintignore" + }, + { + "path": ".eslintrc.yml" + }, + { + "path": ".github/ISSUE_TEMPLATE/bug_report.md" + }, + { + "path": ".github/ISSUE_TEMPLATE/feature_request.md" + }, + { + "path": ".github/ISSUE_TEMPLATE/support_request.md" + }, + { + "path": ".github/PULL_REQUEST_TEMPLATE.md" + }, + { + "path": ".github/release-please.yml" + }, + { + "path": ".gitignore" + }, + { + "path": ".jsdoc.js" + }, + { + "path": ".kokoro/.gitattributes" + }, + { + "path": ".kokoro/browser-test.sh" + }, + { + "path": ".kokoro/common.cfg" + }, + { + "path": ".kokoro/continuous/node10/common.cfg" + }, + { + "path": ".kokoro/continuous/node10/docs.cfg" + }, + { + "path": ".kokoro/continuous/node10/lint.cfg" + }, + { + "path": ".kokoro/continuous/node10/samples-test.cfg" + }, + { + "path": ".kokoro/continuous/node10/system-test.cfg" + }, + { + "path": ".kokoro/continuous/node10/test.cfg" + }, + { + "path": ".kokoro/continuous/node12/common.cfg" + }, + { + "path": ".kokoro/continuous/node12/test.cfg" + }, + { + "path": ".kokoro/continuous/node8/browser-test.cfg" + }, + { + "path": ".kokoro/continuous/node8/common.cfg" + }, + { + "path": ".kokoro/continuous/node8/test.cfg" + }, + { + "path": ".kokoro/docs.sh" + }, + { + "path": ".kokoro/lint.sh" + }, + { + "path": ".kokoro/presubmit/node10/common.cfg" + }, + { + "path": ".kokoro/presubmit/node10/docs.cfg" + }, + { + "path": ".kokoro/presubmit/node10/lint.cfg" + }, + { + "path": ".kokoro/presubmit/node10/samples-test.cfg" + }, + { + "path": ".kokoro/presubmit/node10/system-test.cfg" + }, + { + "path": ".kokoro/presubmit/node10/test.cfg" + }, + { + "path": ".kokoro/presubmit/node12/common.cfg" + }, + { + "path": ".kokoro/presubmit/node12/test.cfg" + }, + { + "path": ".kokoro/presubmit/node8/browser-test.cfg" + }, + { + "path": ".kokoro/presubmit/node8/common.cfg" + }, + { + "path": ".kokoro/presubmit/node8/test.cfg" + }, + { + "path": ".kokoro/presubmit/windows/common.cfg" + }, + { + "path": ".kokoro/presubmit/windows/test.cfg" + }, + { + "path": ".kokoro/publish.sh" + }, + { + "path": ".kokoro/release/common.cfg" + }, + { + "path": ".kokoro/release/docs.cfg" + }, + { + "path": ".kokoro/release/docs.sh" + }, + { + "path": ".kokoro/release/publish.cfg" + }, + { + "path": ".kokoro/samples-test.sh" + }, + { + "path": ".kokoro/system-test.sh" + }, + { + "path": ".kokoro/test.bat" + }, + { + "path": ".kokoro/test.sh" + }, + { + "path": ".kokoro/trampoline.sh" + }, + { + "path": ".nycrc" + }, + { + "path": ".prettierignore" + }, + { + "path": ".prettierrc" + }, + { + "path": ".repo-metadata.json" + }, + { + "path": "CHANGELOG.md" + }, + { + "path": "CODE_OF_CONDUCT.md" + }, + { + "path": "CONTRIBUTING.md" + }, + { + "path": "LICENSE" + }, + { + "path": "README.md" + }, + { + "path": "browser-test/fixtures/keys.ts" + }, + { + "path": "browser-test/test.crypto.ts" + }, + { + "path": "browser-test/test.oauth2.ts" + }, + { + "path": "codecov.yaml" + }, + { + "path": "karma.conf.js" + }, + { + "path": "linkinator.config.json" + }, + { + "path": "package.json" + }, + { + "path": "renovate.json" + }, + { + "path": "samples/.eslintrc.yml" + }, + { + "path": "samples/README.md" + }, + { + "path": "samples/adc.js" + }, + { + "path": "samples/compute.js" + }, + { + "path": "samples/credentials.js" + }, + { + "path": "samples/headers.js" + }, + { + "path": "samples/idtokens-cloudrun.js" + }, + { + "path": "samples/idtokens-iap.js" + }, + { + "path": "samples/jwt.js" + }, + { + "path": "samples/keepalive.js" + }, + { + "path": "samples/keyfile.js" + }, + { + "path": "samples/oauth2-codeVerifier.js" + }, + { + "path": "samples/oauth2.js" + }, + { + "path": "samples/package.json" + }, + { + "path": "samples/puppeteer/.eslintrc.yml" + }, + { + "path": "samples/puppeteer/oauth2-test.js" + }, + { + "path": "samples/puppeteer/package.json" + }, + { + "path": "samples/test/jwt.test.js" + }, + { + "path": "samples/verifyIdToken.js" + }, + { + "path": "src/auth/authclient.ts" + }, + { + "path": "src/auth/computeclient.ts" + }, + { + "path": "src/auth/credentials.ts" + }, + { + "path": "src/auth/envDetect.ts" + }, + { + "path": "src/auth/googleauth.ts" + }, + { + "path": "src/auth/iam.ts" + }, + { + "path": "src/auth/idtokenclient.ts" + }, + { + "path": "src/auth/jwtaccess.ts" + }, + { + "path": "src/auth/jwtclient.ts" + }, + { + "path": "src/auth/loginticket.ts" + }, + { + "path": "src/auth/oauth2client.ts" + }, + { + "path": "src/auth/refreshclient.ts" + }, + { + "path": "src/crypto/browser/crypto.ts" + }, + { + "path": "src/crypto/crypto.ts" + }, + { + "path": "src/crypto/node/crypto.ts" + }, + { + "path": "src/index.ts" + }, + { + "path": "src/messages.ts" + }, + { + "path": "src/options.ts" + }, + { + "path": "src/transporters.ts" + }, + { + "path": "synth.py" + }, + { + "path": "system-test/fixtures/kitchen/package.json" + }, + { + "path": "system-test/fixtures/kitchen/src/index.ts" + }, + { + "path": "system-test/fixtures/kitchen/tsconfig.json" + }, + { + "path": "system-test/fixtures/kitchen/webpack.config.js" + }, + { + "path": "system-test/test.kitchen.ts" + }, + { + "path": "test/fixtures/config-no-quota/.config/gcloud/application_default_credentials.json" + }, + { + "path": "test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json" + }, + { + "path": "test/fixtures/empty.json" + }, + { + "path": "test/fixtures/emptylink" + }, + { + "path": "test/fixtures/goodlink" + }, + { + "path": "test/fixtures/key.p12" + }, + { + "path": "test/fixtures/oauthcertspem.json" + }, + { + "path": "test/fixtures/private.json" + }, + { + "path": "test/fixtures/private.pem" + }, + { + "path": "test/fixtures/private2.json" + }, + { + "path": "test/fixtures/public.pem" + }, + { + "path": "test/fixtures/refresh.json" + }, + { + "path": "test/fixtures/service-account-with-quota.json" + }, + { + "path": "test/fixtures/wellKnown.json" + }, + { + "path": "test/mocha.opts" + }, + { + "path": "test/test.compute.ts" + }, + { + "path": "test/test.crypto.ts" + }, + { + "path": "test/test.googleauth.ts" + }, + { + "path": "test/test.iam.ts" + }, + { + "path": "test/test.idtokenclient.ts" + }, + { + "path": "test/test.index.ts" + }, + { + "path": "test/test.jwt.ts" + }, + { + "path": "test/test.jwtaccess.ts" + }, + { + "path": "test/test.loginticket.ts" + }, + { + "path": "test/test.oauth2.ts" + }, + { + "path": "test/test.refresh.ts" + }, + { + "path": "test/test.transporters.ts" + }, + { + "path": "tsconfig.json" + }, + { + "path": "webpack-tests.config.js" + }, + { + "path": "webpack.config.js" + } + ] +} \ No newline at end of file From 63c4637c57e4113a7b01bf78933a8bff0356c104 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 28 Jan 2020 10:54:09 -0800 Subject: [PATCH 083/662] fix: populate credentials.refresh_token if provided (#881) --- src/auth/refreshclient.ts | 1 + test/test.refresh.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 7175f098..b57fbbd9 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -67,6 +67,7 @@ export class UserRefreshClient extends OAuth2Client { forceRefreshOnFailure: opts.forceRefreshOnFailure, }); this._refreshToken = opts.refreshToken; + this.credentials.refresh_token = opts.refreshToken; } /** diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 81cc3917..67e10b94 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -28,6 +28,13 @@ function createJSON() { }; } +it('populates credentials.refresh_token if provided', () => { + const refresh = new UserRefreshClient({ + refreshToken: 'abc123', + }); + assert.strictEqual(refresh.credentials.refresh_token, 'abc123'); +}); + it('fromJSON should error on null json', () => { const refresh = new UserRefreshClient(); assert.throws(() => { From 634cf6aad88fb2516ea15fec8703f2fcd243b8a4 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2020 11:19:25 -0800 Subject: [PATCH 084/662] chore: release 5.9.2 (#882) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d29a9d..8f261617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.9.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.9.1...v5.9.2) (2020-01-28) + + +### Bug Fixes + +* populate credentials.refresh_token if provided ([#881](https://www.github.com/googleapis/google-auth-library-nodejs/issues/881)) ([63c4637](https://www.github.com/googleapis/google-auth-library-nodejs/commit/63c4637c57e4113a7b01bf78933a8bff0356c104)) + ### [5.9.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.9.0...v5.9.1) (2020-01-16) diff --git a/package.json b/package.json index 8bb87354..e303067d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.9.1", + "version": "5.9.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 86175edb..839143b8 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.9.1", + "google-auth-library": "^5.9.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 13e8cfb32cea5c4edb302a1871e4f33f939078f7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 30 Jan 2020 16:34:11 +0100 Subject: [PATCH 085/662] chore(deps): update dependency @types/mocha to v7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e303067d..ce6c2f17 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", "@types/lru-cache": "^5.0.0", - "@types/mocha": "^5.2.1", + "@types/mocha": "^7.0.0", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", "@types/node": "^10.5.1", From 58e5029327af25eae80d5d71bebfc08f0f573984 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 31 Jan 2020 17:22:38 -0800 Subject: [PATCH 086/662] chore: skip img.shields.io in docs test --- linkinator.config.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/linkinator.config.json b/linkinator.config.json index d780d6bf..b555215c 100644 --- a/linkinator.config.json +++ b/linkinator.config.json @@ -2,6 +2,7 @@ "recurse": true, "skip": [ "https://codecov.io/gh/googleapis/", - "www.googleapis.com" + "www.googleapis.com", + "img.shields.io" ] } From 163e43da69ab9b3890ed6d90ac28af3c069b157e Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 7 Feb 2020 21:09:05 -0800 Subject: [PATCH 087/662] test: modernize mocha config (#884) --- .mocharc.json | 5 + package.json | 1 - test/mocha.opts | 3 - test/test.compute.ts | 437 +++--- test/test.crypto.ts | 3 +- test/test.googleauth.ts | 2819 ++++++++++++++++++------------------ test/test.iam.ts | 63 +- test/test.idtokenclient.ts | 119 +- test/test.index.ts | 38 +- test/test.jwt.ts | 1515 +++++++++---------- test/test.jwtaccess.ts | 287 ++-- test/test.loginticket.ts | 42 +- test/test.oauth2.ts | 2332 ++++++++++++++--------------- test/test.refresh.ts | 225 +-- test/test.transporters.ts | 308 ++-- 15 files changed, 4145 insertions(+), 4052 deletions(-) create mode 100644 .mocharc.json delete mode 100644 test/mocha.opts diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..670c5e2c --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,5 @@ +{ + "enable-source-maps": true, + "throw-deprecation": true, + "timeout": 10000 +} diff --git a/package.json b/package.json index ce6c2f17..02d9e7ca 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "prettier": "^1.13.4", "puppeteer": "^2.0.0", "sinon": "^8.0.0", - "source-map-support": "^0.5.6", "tmp": "^0.1.0", "ts-loader": "^6.0.0", "typescript": "3.6.4", diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 49ca7744..00000000 --- a/test/mocha.opts +++ /dev/null @@ -1,3 +0,0 @@ ---require source-map-support/register ---timeout 30000 ---throw-deprecation diff --git a/test/test.compute.ts b/test/test.compute.ts index 9e191452..974f1daf 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {describe, it, beforeEach, afterEach} from 'mocha'; const assertRejects = require('assert-rejects'); import {BASE_PATH, HEADERS, HOST_ADDRESS} from 'gcp-metadata'; import * as nock from 'nock'; @@ -22,248 +22,259 @@ import {Compute} from '../src'; nock.disableNetConnect(); -const url = 'http://example.com'; -const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`; -const identityPath = `${BASE_PATH}/instance/service-accounts/default/identity`; -function mockToken(statusCode = 200, scopes?: string[]) { - let path = tokenPath; - if (scopes && scopes.length > 0) { - path += `?scopes=${encodeURIComponent(scopes.join(','))}`; +describe('compute', () => { + const url = 'http://example.com'; + const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`; + const identityPath = `${BASE_PATH}/instance/service-accounts/default/identity`; + function mockToken(statusCode = 200, scopes?: string[]) { + let path = tokenPath; + if (scopes && scopes.length > 0) { + path += `?scopes=${encodeURIComponent(scopes.join(','))}`; + } + return nock(HOST_ADDRESS) + .get(path, undefined, {reqheaders: HEADERS}) + .reply(statusCode, {access_token: 'abc123', expires_in: 10000}, HEADERS); } - return nock(HOST_ADDRESS) - .get(path, undefined, {reqheaders: HEADERS}) - .reply(statusCode, {access_token: 'abc123', expires_in: 10000}, HEADERS); -} - -function mockExample() { - return nock(url) - .get('/') - .reply(200); -} - -// set up compute client. -const sandbox = sinon.createSandbox(); -let compute: Compute; -beforeEach(() => { - compute = new Compute(); -}); -afterEach(() => { - nock.cleanAll(); - sandbox.restore(); -}); + function mockExample() { + return nock(url) + .get('/') + .reply(200); + } -it('should create a dummy refresh token string', () => { - // It is important that the compute client is created with a refresh token - // value filled in, or else the rest of the logic will not work. - const compute = new Compute(); - assert.strictEqual('compute-placeholder', compute.credentials.refresh_token); -}); + // set up compute client. + const sandbox = sinon.createSandbox(); + let compute: Compute; + beforeEach(() => { + compute = new Compute(); + }); -it('should get an access token for the first request', async () => { - const scopes = [mockToken(), mockExample()]; - await compute.request({url}); - scopes.forEach(s => s.done()); - assert.strictEqual(compute.credentials.access_token, 'abc123'); -}); + afterEach(() => { + nock.cleanAll(); + sandbox.restore(); + }); -it('should URI-encode and comma-separate scopes when fetching the token', async () => { - const scopes = [ - 'https://www.googleapis.com/reader', - 'https://www.googleapis.com/auth/plus', - ]; + it('should create a dummy refresh token string', () => { + // It is important that the compute client is created with a refresh token + // value filled in, or else the rest of the logic will not work. + const compute = new Compute(); + assert.strictEqual( + 'compute-placeholder', + compute.credentials.refresh_token + ); + }); - const path = `${tokenPath}?scopes=${encodeURIComponent(scopes.join(','))}`; + it('should get an access token for the first request', async () => { + const scopes = [mockToken(), mockExample()]; + await compute.request({url}); + scopes.forEach(s => s.done()); + assert.strictEqual(compute.credentials.access_token, 'abc123'); + }); - const tokenFetchNock = nock(HOST_ADDRESS) - .get(path, undefined, {reqheaders: HEADERS}) - .reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS); - const apiRequestNock = mockExample(); + it('should URI-encode and comma-separate scopes when fetching the token', async () => { + const scopes = [ + 'https://www.googleapis.com/reader', + 'https://www.googleapis.com/auth/plus', + ]; - const compute = new Compute({scopes}); - await compute.request({url}); + const path = `${tokenPath}?scopes=${encodeURIComponent(scopes.join(','))}`; - tokenFetchNock.done(); - apiRequestNock.done(); + const tokenFetchNock = nock(HOST_ADDRESS) + .get(path, undefined, {reqheaders: HEADERS}) + .reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS); + const apiRequestNock = mockExample(); - assert.strictEqual(compute.credentials.access_token, 'abc123'); -}); + const compute = new Compute({scopes}); + await compute.request({url}); -it('should refresh if access token has expired', async () => { - const scopes = [mockToken(), mockExample()]; - compute.credentials.access_token = 'initial-access-token'; - compute.credentials.expiry_date = new Date().getTime() - 10000; - await compute.request({url}); - assert.strictEqual(compute.credentials.access_token, 'abc123'); - scopes.forEach(s => s.done()); -}); + tokenFetchNock.done(); + apiRequestNock.done(); -it('should emit an event for a new access token', async () => { - const scopes = [mockToken(), mockExample()]; - let raisedEvent = false; - compute.on('tokens', tokens => { - assert.strictEqual(tokens.access_token, 'abc123'); - raisedEvent = true; + assert.strictEqual(compute.credentials.access_token, 'abc123'); }); - await compute.request({url}); - assert.strictEqual(compute.credentials.access_token, 'abc123'); - scopes.forEach(s => s.done()); - assert(raisedEvent); -}); -it('should refresh if access token will expired soon and time to refresh before expiration is set', async () => { - const scopes = [mockToken(), mockExample()]; - compute = new Compute({eagerRefreshThresholdMillis: 10000}); - compute.credentials.access_token = 'initial-access-token'; - compute.credentials.expiry_date = new Date().getTime() + 5000; - await compute.request({url}); - assert.strictEqual(compute.credentials.access_token, 'abc123'); - scopes.forEach(s => s.done()); -}); + it('should refresh if access token has expired', async () => { + const scopes = [mockToken(), mockExample()]; + compute.credentials.access_token = 'initial-access-token'; + compute.credentials.expiry_date = new Date().getTime() - 10000; + await compute.request({url}); + assert.strictEqual(compute.credentials.access_token, 'abc123'); + scopes.forEach(s => s.done()); + }); -it('should not refresh if access token will not expire soon and time to refresh before expiration is set', async () => { - const scope = mockExample(); - compute = new Compute({eagerRefreshThresholdMillis: 1000}); - compute.credentials.access_token = 'initial-access-token'; - compute.credentials.expiry_date = new Date().getTime() + 12000; - await compute.request({url}); - assert.strictEqual(compute.credentials.access_token, 'initial-access-token'); - scope.done(); -}); + it('should emit an event for a new access token', async () => { + const scopes = [mockToken(), mockExample()]; + let raisedEvent = false; + compute.on('tokens', tokens => { + assert.strictEqual(tokens.access_token, 'abc123'); + raisedEvent = true; + }); + await compute.request({url}); + assert.strictEqual(compute.credentials.access_token, 'abc123'); + scopes.forEach(s => s.done()); + assert(raisedEvent); + }); -it('should not refresh if access token has not expired', async () => { - const scope = mockExample(); - compute.credentials.access_token = 'initial-access-token'; - compute.credentials.expiry_date = new Date().getTime() + 10 * 60 * 1000; - await compute.request({url}); - assert.strictEqual(compute.credentials.access_token, 'initial-access-token'); - scope.done(); -}); + it('should refresh if access token will expired soon and time to refresh before expiration is set', async () => { + const scopes = [mockToken(), mockExample()]; + compute = new Compute({eagerRefreshThresholdMillis: 10000}); + compute.credentials.access_token = 'initial-access-token'; + compute.credentials.expiry_date = new Date().getTime() + 5000; + await compute.request({url}); + assert.strictEqual(compute.credentials.access_token, 'abc123'); + scopes.forEach(s => s.done()); + }); -it('should emit warning for createScopedRequired', () => { - let called = false; - sandbox.stub(process, 'emitWarning').callsFake(() => (called = true)); - // tslint:disable-next-line deprecation - compute.createScopedRequired(); - assert.strictEqual(called, true); -}); + it('should not refresh if access token will not expire soon and time to refresh before expiration is set', async () => { + const scope = mockExample(); + compute = new Compute({eagerRefreshThresholdMillis: 1000}); + compute.credentials.access_token = 'initial-access-token'; + compute.credentials.expiry_date = new Date().getTime() + 12000; + await compute.request({url}); + assert.strictEqual( + compute.credentials.access_token, + 'initial-access-token' + ); + scope.done(); + }); -it('should return false for createScopedRequired', () => { - // tslint:disable-next-line deprecation - assert.strictEqual(false, compute.createScopedRequired()); -}); + it('should not refresh if access token has not expired', async () => { + const scope = mockExample(); + compute.credentials.access_token = 'initial-access-token'; + compute.credentials.expiry_date = new Date().getTime() + 10 * 60 * 1000; + await compute.request({url}); + assert.strictEqual( + compute.credentials.access_token, + 'initial-access-token' + ); + scope.done(); + }); -it('should return a helpful message on request response.statusCode 403', async () => { - const scope = mockToken(403); - const expected = new RegExp( - 'A Forbidden error was returned while attempting to retrieve an access ' + - 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have the correct permission scopes specified. ' + - 'Could not refresh access token.' - ); - await assertRejects(compute.request({url}), expected); - scope.done(); -}); + it('should emit warning for createScopedRequired', () => { + let called = false; + sandbox.stub(process, 'emitWarning').callsFake(() => (called = true)); + // tslint:disable-next-line deprecation + compute.createScopedRequired(); + assert.strictEqual(called, true); + }); -it('should return a helpful message on request response.statusCode 404', async () => { - const scope = mockToken(404); - const expected = new RegExp( - 'A Not Found error was returned while attempting to retrieve an access' + - 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have any permission scopes specified.' - ); - await assertRejects(compute.request({url}), expected); - scope.done(); -}); + it('should return false for createScopedRequired', () => { + // tslint:disable-next-line deprecation + assert.strictEqual(false, compute.createScopedRequired()); + }); -it('should return a helpful message on token refresh response.statusCode 403', async () => { - const scope = mockToken(403); - // Mock the credentials object with a null access token, to force a - // refresh. - compute.credentials = { - refresh_token: 'hello', - access_token: undefined, - expiry_date: 1, - }; - const expected = new RegExp( - 'A Forbidden error was returned while attempting to retrieve an access ' + - 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have the correct permission scopes specified. ' + - 'Could not refresh access token.' - ); - await assertRejects(compute.request({}), expected); - scope.done(); -}); + it('should return a helpful message on request response.statusCode 403', async () => { + const scope = mockToken(403); + const expected = new RegExp( + 'A Forbidden error was returned while attempting to retrieve an access ' + + 'token for the Compute Engine built-in service account. This may be because the ' + + 'Compute Engine instance does not have the correct permission scopes specified. ' + + 'Could not refresh access token.' + ); + await assertRejects(compute.request({url}), expected); + scope.done(); + }); -it('should return a helpful message on token refresh response.statusCode 404', async () => { - const scope = mockToken(404); - - // Mock the credentials object with a null access token, to force a - // refresh. - compute.credentials = { - refresh_token: 'hello', - access_token: undefined, - expiry_date: 1, - }; - - const expected = new RegExp( - 'A Not Found error was returned while attempting to retrieve an access' + - 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have any permission scopes specified. Could not ' + - 'refresh access token.' - ); - - await assertRejects(compute.request({}), expected); - scope.done(); -}); + it('should return a helpful message on request response.statusCode 404', async () => { + const scope = mockToken(404); + const expected = new RegExp( + 'A Not Found error was returned while attempting to retrieve an access' + + 'token for the Compute Engine built-in service account. This may be because the ' + + 'Compute Engine instance does not have any permission scopes specified.' + ); + await assertRejects(compute.request({url}), expected); + scope.done(); + }); -it('should accept a custom service account', async () => { - const serviceAccountEmail = 'service-account@example.com'; - const compute = new Compute({serviceAccountEmail}); - const scopes = [ - mockExample(), - nock(HOST_ADDRESS) - .get( - `${BASE_PATH}/instance/service-accounts/${serviceAccountEmail}/token` - ) - .reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS), - ]; - await compute.request({url}); - scopes.forEach(s => s.done()); - assert.strictEqual(compute.credentials.access_token, 'abc123'); -}); + it('should return a helpful message on token refresh response.statusCode 403', async () => { + const scope = mockToken(403); + // Mock the credentials object with a null access token, to force a + // refresh. + compute.credentials = { + refresh_token: 'hello', + access_token: undefined, + expiry_date: 1, + }; + const expected = new RegExp( + 'A Forbidden error was returned while attempting to retrieve an access ' + + 'token for the Compute Engine built-in service account. This may be because the ' + + 'Compute Engine instance does not have the correct permission scopes specified. ' + + 'Could not refresh access token.' + ); + await assertRejects(compute.request({}), expected); + scope.done(); + }); -it('should request the identity endpoint for fetchIdToken', async () => { - const targetAudience = 'a-target-audience'; - const path = `${identityPath}?format=full&audience=${targetAudience}`; + it('should return a helpful message on token refresh response.statusCode 404', async () => { + const scope = mockToken(404); + + // Mock the credentials object with a null access token, to force a + // refresh. + compute.credentials = { + refresh_token: 'hello', + access_token: undefined, + expiry_date: 1, + }; + + const expected = new RegExp( + 'A Not Found error was returned while attempting to retrieve an access' + + 'token for the Compute Engine built-in service account. This may be because the ' + + 'Compute Engine instance does not have any permission scopes specified. Could not ' + + 'refresh access token.' + ); + + await assertRejects(compute.request({}), expected); + scope.done(); + }); - const tokenFetchNock = nock(HOST_ADDRESS) - .get(path, undefined, {reqheaders: HEADERS}) - .reply(200, 'abc123', HEADERS); + it('should accept a custom service account', async () => { + const serviceAccountEmail = 'service-account@example.com'; + const compute = new Compute({serviceAccountEmail}); + const scopes = [ + mockExample(), + nock(HOST_ADDRESS) + .get( + `${BASE_PATH}/instance/service-accounts/${serviceAccountEmail}/token` + ) + .reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS), + ]; + await compute.request({url}); + scopes.forEach(s => s.done()); + assert.strictEqual(compute.credentials.access_token, 'abc123'); + }); - const compute = new Compute(); - const idToken = await compute.fetchIdToken(targetAudience); + it('should request the identity endpoint for fetchIdToken', async () => { + const targetAudience = 'a-target-audience'; + const path = `${identityPath}?format=full&audience=${targetAudience}`; - tokenFetchNock.done(); + const tokenFetchNock = nock(HOST_ADDRESS) + .get(path, undefined, {reqheaders: HEADERS}) + .reply(200, 'abc123', HEADERS); - assert.strictEqual(idToken, 'abc123'); -}); + const compute = new Compute(); + const idToken = await compute.fetchIdToken(targetAudience); + + tokenFetchNock.done(); -it('should throw an error if metadata server is unavailable', async () => { - const targetAudience = 'a-target-audience'; - const path = `${identityPath}?format=full&audience=${targetAudience}`; + assert.strictEqual(idToken, 'abc123'); + }); - const tokenFetchNock = nock(HOST_ADDRESS) - .get(path, undefined, {reqheaders: HEADERS}) - .reply(500, 'a server error!', HEADERS); + it('should throw an error if metadata server is unavailable', async () => { + const targetAudience = 'a-target-audience'; + const path = `${identityPath}?format=full&audience=${targetAudience}`; - const compute = new Compute(); - try { - await compute.fetchIdToken(targetAudience); - } catch { - tokenFetchNock.done(); - return; - } + const tokenFetchNock = nock(HOST_ADDRESS) + .get(path, undefined, {reqheaders: HEADERS}) + .reply(500, 'a server error!', HEADERS); - assert.fail('failed to throw'); + const compute = new Compute(); + try { + await compute.fetchIdToken(targetAudience); + } catch { + tokenFetchNock.done(); + return; + } + + assert.fail('failed to throw'); + }); }); diff --git a/test/test.crypto.ts b/test/test.crypto.ts index ee0b723f..f4faf670 100644 --- a/test/test.crypto.ts +++ b/test/test.crypto.ts @@ -1,12 +1,13 @@ import * as fs from 'fs'; import {assert} from 'chai'; +import {describe, it} from 'mocha'; import {createCrypto} from '../src/crypto/crypto'; import {NodeCrypto} from '../src/crypto/node/crypto'; const publicKey = fs.readFileSync('./test/fixtures/public.pem', 'utf-8'); const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); -describe('Node.js crypto tests', () => { +describe('crypto', () => { const crypto = createCrypto(); it('should create a NodeCrypto instance', () => { diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 5b5c2bb6..95ac4b5e 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {describe, it, beforeEach, afterEach} from 'mocha'; const assertRejects = require('assert-rejects'); import * as child_process from 'child_process'; import * as crypto from 'crypto'; @@ -38,1552 +38,1577 @@ import * as messages from '../src/messages'; nock.disableNetConnect(); -const isWindows = process.platform === 'win32'; - -const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`; -const host = HOST_ADDRESS; -const instancePath = `${BASE_PATH}/instance`; -const svcAccountPath = `${instancePath}/service-accounts/?recursive=true`; -const API_KEY = 'test-123'; -const STUB_PROJECT = 'my-awesome-project'; -const ENDPOINT = '/events:report'; -const RESPONSE_BODY = 'RESPONSE_BODY'; -const BASE_URL = [ - 'https://clouderrorreporting.googleapis.com/v1beta1/projects', - STUB_PROJECT, -].join('/'); - -const privateJSON = require('../../test/fixtures/private.json'); -const private2JSON = require('../../test/fixtures/private2.json'); -const refreshJSON = require('../../test/fixtures/refresh.json'); -const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); -const wellKnownPathWindows = path.join( - 'C:', - 'fake', - 'home', - 'gcloud', - 'application_default_credentials.json' -); -const wellKnownPathLinux = path.join( - '/', - 'fake', - 'user', - '.config', - 'gcloud', - 'application_default_credentials.json' -); - describe('googleauth', () => { - let auth: GoogleAuth; - const sandbox = sinon.createSandbox(); - let osStub: sinon.SinonStub<[], NodeJS.Platform>; - let exposeWindowsWellKnownFile: boolean; - let exposeLinuxWellKnownFile: boolean; - let createLinuxWellKnownStream: Function; - let createWindowsWellKnownStream: Function; - beforeEach(() => { - resetIsAvailableCache(); - auth = new GoogleAuth(); - exposeWindowsWellKnownFile = false; - exposeLinuxWellKnownFile = false; - createLinuxWellKnownStream = () => { - throw new Error(); - }; - createWindowsWellKnownStream = () => { - throw new Error(); - }; - const envVars = Object.assign({}, process.env, { - GCLOUD_PROJECT: undefined, - GOOGLE_APPLICATION_CREDENTIALS: undefined, - google_application_credentials: undefined, - HOME: path.join('/', 'fake', 'user'), - }); - sandbox.stub(process, 'env').value(envVars); - osStub = sandbox.stub(os, 'platform').returns('linux'); - sandbox - .stub(fs, 'existsSync') - .callThrough() - .withArgs(wellKnownPathLinux) - .callsFake(() => exposeLinuxWellKnownFile) - .withArgs(wellKnownPathWindows) - .callsFake(() => exposeWindowsWellKnownFile); - - sandbox - .stub(fs, 'createReadStream') - .callThrough() - .withArgs(wellKnownPathLinux) - .callsFake(() => createLinuxWellKnownStream()) - .withArgs(wellKnownPathWindows) - .callsFake(() => createWindowsWellKnownStream()); - - sandbox - .stub(fs, 'realpathSync') - .callThrough() - .withArgs(wellKnownPathLinux) - .returnsArg(0) - .withArgs(wellKnownPathWindows) - .returnsArg(0); - - sandbox - .stub(child_process, 'exec') - .callThrough() - .withArgs('gcloud config config-helper --format json', sinon.match.func) - .callsArgWith(1, null, '', null); - - const fakeStat = {isFile: () => true} as fs.Stats; - sandbox - .stub(fs, 'lstatSync') - .callThrough() - .withArgs(wellKnownPathLinux) - .returns(fakeStat) - .withArgs(wellKnownPathWindows) - .returns(fakeStat); - }); + const isWindows = process.platform === 'win32'; + + const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`; + const host = HOST_ADDRESS; + const instancePath = `${BASE_PATH}/instance`; + const svcAccountPath = `${instancePath}/service-accounts/?recursive=true`; + const API_KEY = 'test-123'; + const STUB_PROJECT = 'my-awesome-project'; + const ENDPOINT = '/events:report'; + const RESPONSE_BODY = 'RESPONSE_BODY'; + const BASE_URL = [ + 'https://clouderrorreporting.googleapis.com/v1beta1/projects', + STUB_PROJECT, + ].join('/'); + + const privateJSON = require('../../test/fixtures/private.json'); + const private2JSON = require('../../test/fixtures/private2.json'); + const refreshJSON = require('../../test/fixtures/refresh.json'); + const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); + const wellKnownPathWindows = path.join( + 'C:', + 'fake', + 'home', + 'gcloud', + 'application_default_credentials.json' + ); + const wellKnownPathLinux = path.join( + '/', + 'fake', + 'user', + '.config', + 'gcloud', + 'application_default_credentials.json' + ); + + describe('googleauth', () => { + let auth: GoogleAuth; + const sandbox = sinon.createSandbox(); + let osStub: sinon.SinonStub<[], NodeJS.Platform>; + let exposeWindowsWellKnownFile: boolean; + let exposeLinuxWellKnownFile: boolean; + let createLinuxWellKnownStream: Function; + let createWindowsWellKnownStream: Function; + beforeEach(() => { + resetIsAvailableCache(); + auth = new GoogleAuth(); + exposeWindowsWellKnownFile = false; + exposeLinuxWellKnownFile = false; + createLinuxWellKnownStream = () => { + throw new Error(); + }; + createWindowsWellKnownStream = () => { + throw new Error(); + }; + const envVars = Object.assign({}, process.env, { + GCLOUD_PROJECT: undefined, + GOOGLE_APPLICATION_CREDENTIALS: undefined, + google_application_credentials: undefined, + HOME: path.join('/', 'fake', 'user'), + }); + sandbox.stub(process, 'env').value(envVars); + osStub = sandbox.stub(os, 'platform').returns('linux'); + sandbox + .stub(fs, 'existsSync') + .callThrough() + .withArgs(wellKnownPathLinux) + .callsFake(() => exposeLinuxWellKnownFile) + .withArgs(wellKnownPathWindows) + .callsFake(() => exposeWindowsWellKnownFile); + + sandbox + .stub(fs, 'createReadStream') + .callThrough() + .withArgs(wellKnownPathLinux) + .callsFake(() => createLinuxWellKnownStream()) + .withArgs(wellKnownPathWindows) + .callsFake(() => createWindowsWellKnownStream()); + + sandbox + .stub(fs, 'realpathSync') + .callThrough() + .withArgs(wellKnownPathLinux) + .returnsArg(0) + .withArgs(wellKnownPathWindows) + .returnsArg(0); + + sandbox + .stub(child_process, 'exec') + .callThrough() + .withArgs('gcloud config config-helper --format json', sinon.match.func) + .callsArgWith(1, null, '', null); + + const fakeStat = {isFile: () => true} as fs.Stats; + sandbox + .stub(fs, 'lstatSync') + .callThrough() + .withArgs(wellKnownPathLinux) + .returns(fakeStat) + .withArgs(wellKnownPathWindows) + .returns(fakeStat); + }); - afterEach(() => { - nock.cleanAll(); - sandbox.restore(); - }); + afterEach(() => { + nock.cleanAll(); + sandbox.restore(); + }); - function mockWindows() { - osStub.returns('win32'); - process.env.HOME = ''; - process.env.APPDATA = path.join('C:', 'fake', 'home'); - } - - function mockWindowsWellKnownFile() { - exposeWindowsWellKnownFile = true; - createWindowsWellKnownStream = () => - fs.createReadStream('./test/fixtures/private2.json'); - } - - function mockLinuxWellKnownFile() { - exposeLinuxWellKnownFile = true; - createLinuxWellKnownStream = () => - fs.createReadStream('./test/fixtures/private2.json'); - } - - function nockIsGCE() { - const primary = nock(host) - .get(instancePath) - .reply(200, {}, HEADERS); - const secondary = nock(SECONDARY_HOST_ADDRESS) - .get(instancePath) - .reply(200, {}, HEADERS); - - return { - done: () => { - try { - primary.done(); - secondary.done(); - } catch (_err) { - // secondary can sometimes complete prior to primary. - } - }, - }; - } - - function nockNotGCE() { - const primary = nock(host) - .get(instancePath) - .replyWithError({code: 'ENOTFOUND'}); - const secondary = nock(SECONDARY_HOST_ADDRESS) - .get(instancePath) - .replyWithError({code: 'ENOTFOUND'}); - return { - done: () => { - try { - primary.done(); - secondary.done(); - } catch (_err) { - // secondary can sometimes complete prior to primary. - } - }, - }; - } - - function nock500GCE() { - const primary = nock(host) - .get(instancePath) - .reply(500, {}, HEADERS); - const secondary = nock(SECONDARY_HOST_ADDRESS) - .get(instancePath) - .reply(500, {}, HEADERS); - - return { - done: () => { - try { - primary.done(); - secondary.done(); - } catch (err) { - // secondary can sometimes complete prior to primary. - } - }, - }; - } - - function nock404GCE() { - const primary = nock(host) - .get(instancePath) - .reply(404); - const secondary = nock(SECONDARY_HOST_ADDRESS) - .get(instancePath) - .reply(404); - return { - done: () => { - try { - primary.done(); - secondary.done(); - } catch (err) { - // secondary can sometimes complete prior to primary. - } - }, - }; - } - - function createGetProjectIdNock(projectId = 'not-real') { - return nock(host) - .get(`${BASE_PATH}/project/project-id`) - .reply(200, projectId, HEADERS); - } - - // Creates a standard JSON auth object for testing. - function createJwtJSON() { - return { - private_key_id: 'key123', - private_key: 'privatekey', - client_email: 'hello@youarecool.com', - client_id: 'client123', - type: 'service_account', - }; - } - - // Pretend that we're GCE, and mock an access token. - function mockGCE() { - const scope1 = nockIsGCE(); - const auth = new GoogleAuth(); - // tslint:disable-next-line no-any - sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); - const scope2 = nock(HOST_ADDRESS) - .get(tokenPath) - .reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS); - return {auth, scopes: [scope1, scope2]}; - } - - // Intercepts the specified environment variable, returning the specified - // value. - function mockEnvVar(name: string, value = '') { - const envVars = Object.assign({}, process.env, {[name]: value}); - return sandbox.stub(process, 'env').value(envVars); - } - - it('fromJSON should support the instantiated named export', () => { - const result = auth.fromJSON(createJwtJSON()); - assert(result); - }); + function mockWindows() { + osStub.returns('win32'); + process.env.HOME = ''; + process.env.APPDATA = path.join('C:', 'fake', 'home'); + } - it('fromJson should error on null json', () => { - const auth = new GoogleAuth(); - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. + function mockWindowsWellKnownFile() { + exposeWindowsWellKnownFile = true; + createWindowsWellKnownStream = () => + fs.createReadStream('./test/fixtures/private2.json'); + } + + function mockLinuxWellKnownFile() { + exposeLinuxWellKnownFile = true; + createLinuxWellKnownStream = () => + fs.createReadStream('./test/fixtures/private2.json'); + } + + function nockIsGCE() { + const primary = nock(host) + .get(instancePath) + .reply(200, {}, HEADERS); + const secondary = nock(SECONDARY_HOST_ADDRESS) + .get(instancePath) + .reply(200, {}, HEADERS); + + return { + done: () => { + try { + primary.done(); + secondary.done(); + } catch (_err) { + // secondary can sometimes complete prior to primary. + } + }, + }; + } + + function nockNotGCE() { + const primary = nock(host) + .get(instancePath) + .replyWithError({code: 'ENOTFOUND'}); + const secondary = nock(SECONDARY_HOST_ADDRESS) + .get(instancePath) + .replyWithError({code: 'ENOTFOUND'}); + return { + done: () => { + try { + primary.done(); + secondary.done(); + } catch (_err) { + // secondary can sometimes complete prior to primary. + } + }, + }; + } + + function nock500GCE() { + const primary = nock(host) + .get(instancePath) + .reply(500, {}, HEADERS); + const secondary = nock(SECONDARY_HOST_ADDRESS) + .get(instancePath) + .reply(500, {}, HEADERS); + + return { + done: () => { + try { + primary.done(); + secondary.done(); + } catch (err) { + // secondary can sometimes complete prior to primary. + } + }, + }; + } + + function nock404GCE() { + const primary = nock(host) + .get(instancePath) + .reply(404); + const secondary = nock(SECONDARY_HOST_ADDRESS) + .get(instancePath) + .reply(404); + return { + done: () => { + try { + primary.done(); + secondary.done(); + } catch (err) { + // secondary can sometimes complete prior to primary. + } + }, + }; + } + + function createGetProjectIdNock(projectId = 'not-real') { + return nock(host) + .get(`${BASE_PATH}/project/project-id`) + .reply(200, projectId, HEADERS); + } + + // Creates a standard JSON auth object for testing. + function createJwtJSON() { + return { + private_key_id: 'key123', + private_key: 'privatekey', + client_email: 'hello@youarecool.com', + client_id: 'client123', + type: 'service_account', + }; + } + + // Pretend that we're GCE, and mock an access token. + function mockGCE() { + const scope1 = nockIsGCE(); + const auth = new GoogleAuth(); // tslint:disable-next-line no-any - (auth as any).fromJSON(null); + sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); + const scope2 = nock(HOST_ADDRESS) + .get(tokenPath) + .reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS); + return {auth, scopes: [scope1, scope2]}; + } + + // Intercepts the specified environment variable, returning the specified + // value. + function mockEnvVar(name: string, value = '') { + const envVars = Object.assign({}, process.env, {[name]: value}); + return sandbox.stub(process, 'env').value(envVars); + } + + it('fromJSON should support the instantiated named export', () => { + const result = auth.fromJSON(createJwtJSON()); + assert(result); }); - }); - it('fromJson should not overwrite previous client configuration', async () => { - const auth = new GoogleAuth({keyFilename: './test/fixtures/private.json'}); - auth.fromJSON({ - client_email: 'batman@example.com', - private_key: 'abc123', + it('fromJson should error on null json', () => { + const auth = new GoogleAuth(); + assert.throws(() => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (auth as any).fromJSON(null); + }); }); - const client = (await auth.getClient()) as JWT; - assert.strictEqual(client.email, 'hello@youarecool.com'); - }); - it('fromAPIKey should error given an invalid api key', () => { - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (auth as any).fromAPIKey(null); + it('fromJson should not overwrite previous client configuration', async () => { + const auth = new GoogleAuth({ + keyFilename: './test/fixtures/private.json', + }); + auth.fromJSON({ + client_email: 'batman@example.com', + private_key: 'abc123', + }); + const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.email, 'hello@youarecool.com'); }); - }); - it('should make a request with the api key', async () => { - const scope = nock(BASE_URL) - .post(ENDPOINT) - .reply(function(uri) { - assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); - return [200, RESPONSE_BODY]; + it('fromAPIKey should error given an invalid api key', () => { + assert.throws(() => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (auth as any).fromAPIKey(null); }); - const client = auth.fromAPIKey(API_KEY); - const res = await client.request({ - url: BASE_URL + ENDPOINT, - method: 'POST', - data: {test: true}, - }); - assert.strictEqual(RESPONSE_BODY, res.data); - scope.done(); - }); + }); - it('should put the api key in the headers', async () => { - const client = auth.fromAPIKey(API_KEY); - const headers = await client.getRequestHeaders(); - assert.strictEqual(headers['X-Goog-Api-Key'], API_KEY); - }); + it('should make a request with the api key', async () => { + const scope = nock(BASE_URL) + .post(ENDPOINT) + .reply(function(uri) { + assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); + return [200, RESPONSE_BODY]; + }); + const client = auth.fromAPIKey(API_KEY); + const res = await client.request({ + url: BASE_URL + ENDPOINT, + method: 'POST', + data: {test: true}, + }); + assert.strictEqual(RESPONSE_BODY, res.data); + scope.done(); + }); - it('should make a request while preserving original parameters', async () => { - const OTHER_QS_PARAM = {test: 'abc'}; - const scope = nock(BASE_URL) - .post(ENDPOINT) - .query({test: OTHER_QS_PARAM.test}) - .reply(function(uri) { - assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); - assert(uri.indexOf('test=' + OTHER_QS_PARAM.test) > -1); - return [200, RESPONSE_BODY]; + it('should put the api key in the headers', async () => { + const client = auth.fromAPIKey(API_KEY); + const headers = await client.getRequestHeaders(); + assert.strictEqual(headers['X-Goog-Api-Key'], API_KEY); + }); + + it('should make a request while preserving original parameters', async () => { + const OTHER_QS_PARAM = {test: 'abc'}; + const scope = nock(BASE_URL) + .post(ENDPOINT) + .query({test: OTHER_QS_PARAM.test}) + .reply(function(uri) { + assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); + assert(uri.indexOf('test=' + OTHER_QS_PARAM.test) > -1); + return [200, RESPONSE_BODY]; + }); + const client = auth.fromAPIKey(API_KEY); + const res = await client.request({ + url: BASE_URL + ENDPOINT, + method: 'POST', + data: {test: true}, + params: OTHER_QS_PARAM, }); - const client = auth.fromAPIKey(API_KEY); - const res = await client.request({ - url: BASE_URL + ENDPOINT, - method: 'POST', - data: {test: true}, - params: OTHER_QS_PARAM, - }); - assert.strictEqual(RESPONSE_BODY, res.data); - scope.done(); - }); + assert.strictEqual(RESPONSE_BODY, res.data); + scope.done(); + }); - it('should make client with eagerRetryThresholdMillis set', () => { - const client = auth.fromAPIKey(API_KEY, {eagerRefreshThresholdMillis: 100}); - assert.strictEqual(100, client.eagerRefreshThresholdMillis); - }); + it('should make client with eagerRetryThresholdMillis set', () => { + const client = auth.fromAPIKey(API_KEY, { + eagerRefreshThresholdMillis: 100, + }); + assert.strictEqual(100, client.eagerRefreshThresholdMillis); + }); - it('fromJSON should error on empty json', () => { - const auth = new GoogleAuth(); - assert.throws(() => { - auth.fromJSON({}); + it('fromJSON should error on empty json', () => { + const auth = new GoogleAuth(); + assert.throws(() => { + auth.fromJSON({}); + }); }); - }); - it('fromJSON should error on missing client_email', () => { - const json = createJwtJSON(); - delete json.client_email; - assert.throws(() => { - auth.fromJSON(json); + it('fromJSON should error on missing client_email', () => { + const json = createJwtJSON(); + delete json.client_email; + assert.throws(() => { + auth.fromJSON(json); + }); }); - }); - it('fromJSON should error on missing private_key', () => { - const json = createJwtJSON(); - delete json.private_key; - assert.throws(() => { - auth.fromJSON(json); + it('fromJSON should error on missing private_key', () => { + const json = createJwtJSON(); + delete json.private_key; + assert.throws(() => { + auth.fromJSON(json); + }); }); - }); - it('fromJSON should create JWT with client_email', () => { - const json = createJwtJSON(); - const result = auth.fromJSON(json); - assert.strictEqual(json.client_email, (result as JWT).email); - }); + it('fromJSON should create JWT with client_email', () => { + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.strictEqual(json.client_email, (result as JWT).email); + }); - it('fromJSON should create JWT with private_key', () => { - const json = createJwtJSON(); - const result = auth.fromJSON(json); - assert.strictEqual(json.private_key, (result as JWT).key); - }); + it('fromJSON should create JWT with private_key', () => { + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.strictEqual(json.private_key, (result as JWT).key); + }); - it('fromJSON should create JWT with null scopes', () => { - const json = createJwtJSON(); - const result = auth.fromJSON(json); - assert.strictEqual(undefined, (result as JWT).scopes); - }); + it('fromJSON should create JWT with null scopes', () => { + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.strictEqual(undefined, (result as JWT).scopes); + }); - it('fromJSON should create JWT with null subject', () => { - const json = createJwtJSON(); - const result = auth.fromJSON(json); - assert.strictEqual(undefined, (result as JWT).subject); - }); + it('fromJSON should create JWT with null subject', () => { + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.strictEqual(undefined, (result as JWT).subject); + }); - it('fromJSON should create JWT with null keyFile', () => { - const json = createJwtJSON(); - const result = auth.fromJSON(json); - assert.strictEqual(undefined, (result as JWT).keyFile); - }); + it('fromJSON should create JWT with null keyFile', () => { + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.strictEqual(undefined, (result as JWT).keyFile); + }); - it('fromJSON should create JWT which eagerRefreshThresholdMillisset when this is set for GoogleAuth', () => { - const json = createJwtJSON(); - const result = auth.fromJSON(json, {eagerRefreshThresholdMillis: 5000}); - assert.strictEqual(5000, (result as JWT).eagerRefreshThresholdMillis); - }); + it('fromJSON should create JWT which eagerRefreshThresholdMillisset when this is set for GoogleAuth', () => { + const json = createJwtJSON(); + const result = auth.fromJSON(json, {eagerRefreshThresholdMillis: 5000}); + assert.strictEqual(5000, (result as JWT).eagerRefreshThresholdMillis); + }); - it('fromJSON should create JWT with 5min as value for eagerRefreshThresholdMillis', () => { - const json = createJwtJSON(); - const result = auth.fromJSON(json); - assert.strictEqual(300000, (result as JWT).eagerRefreshThresholdMillis); - }); + it('fromJSON should create JWT with 5min as value for eagerRefreshThresholdMillis', () => { + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.strictEqual(300000, (result as JWT).eagerRefreshThresholdMillis); + }); - it('fromStream should error on null stream', done => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (auth as any).fromStream(null, (err: Error) => { - assert.strictEqual(true, err instanceof Error); - done(); + it('fromStream should error on null stream', done => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (auth as any).fromStream(null, (err: Error) => { + assert.strictEqual(true, err instanceof Error); + done(); + }); }); - }); - it('fromStream should read the stream and create a jwt', async () => { - const stream = fs.createReadStream('./test/fixtures/private.json'); - const res = await auth.fromStream(stream); - const jwt = res as JWT; - // Ensure that the correct bits were pulled from the stream. - assert.strictEqual(privateJSON.private_key, jwt.key); - assert.strictEqual(privateJSON.client_email, jwt.email); - assert.strictEqual(undefined, jwt.keyFile); - assert.strictEqual(undefined, jwt.subject); - assert.strictEqual(undefined, jwt.scope); - }); + it('fromStream should read the stream and create a jwt', async () => { + const stream = fs.createReadStream('./test/fixtures/private.json'); + const res = await auth.fromStream(stream); + const jwt = res as JWT; + // Ensure that the correct bits were pulled from the stream. + assert.strictEqual(privateJSON.private_key, jwt.key); + assert.strictEqual(privateJSON.client_email, jwt.email); + assert.strictEqual(undefined, jwt.keyFile); + assert.strictEqual(undefined, jwt.subject); + assert.strictEqual(undefined, jwt.scope); + }); - it('fromStream should read the stream and create a jwt with eager refresh', async () => { - const stream = fs.createReadStream('./test/fixtures/private.json'); - const auth = new GoogleAuth(); - const result = await auth.fromStream(stream, { - eagerRefreshThresholdMillis: 1000 * 60 * 60, - }); - const jwt = result as JWT; - // Ensure that the correct bits were pulled from the stream. - assert.strictEqual(privateJSON.private_key, jwt.key); - assert.strictEqual(privateJSON.client_email, jwt.email); - assert.strictEqual(undefined, jwt.keyFile); - assert.strictEqual(undefined, jwt.subject); - assert.strictEqual(undefined, jwt.scope); - assert.strictEqual(1000 * 60 * 60, jwt.eagerRefreshThresholdMillis); - }); + it('fromStream should read the stream and create a jwt with eager refresh', async () => { + const stream = fs.createReadStream('./test/fixtures/private.json'); + const auth = new GoogleAuth(); + const result = await auth.fromStream(stream, { + eagerRefreshThresholdMillis: 1000 * 60 * 60, + }); + const jwt = result as JWT; + // Ensure that the correct bits were pulled from the stream. + assert.strictEqual(privateJSON.private_key, jwt.key); + assert.strictEqual(privateJSON.client_email, jwt.email); + assert.strictEqual(undefined, jwt.keyFile); + assert.strictEqual(undefined, jwt.subject); + assert.strictEqual(undefined, jwt.scope); + assert.strictEqual(1000 * 60 * 60, jwt.eagerRefreshThresholdMillis); + }); - it('should read another stream and create a UserRefreshClient', async () => { - const stream = fs.createReadStream('./test/fixtures/refresh.json'); - const auth = new GoogleAuth(); - const res = await auth.fromStream(stream); - // Ensure that the correct bits were pulled from the stream. - const rc = res as UserRefreshClient; - assert.strictEqual(refreshJSON.client_id, rc._clientId); - assert.strictEqual(refreshJSON.client_secret, rc._clientSecret); - assert.strictEqual(refreshJSON.refresh_token, rc._refreshToken); - }); + it('should read another stream and create a UserRefreshClient', async () => { + const stream = fs.createReadStream('./test/fixtures/refresh.json'); + const auth = new GoogleAuth(); + const res = await auth.fromStream(stream); + // Ensure that the correct bits were pulled from the stream. + const rc = res as UserRefreshClient; + assert.strictEqual(refreshJSON.client_id, rc._clientId); + assert.strictEqual(refreshJSON.client_secret, rc._clientSecret); + assert.strictEqual(refreshJSON.refresh_token, rc._refreshToken); + }); - it('should read another stream and create a UserRefreshClient with eager refresh', async () => { - const stream = fs.createReadStream('./test/fixtures/refresh.json'); - const auth = new GoogleAuth(); - const result = await auth.fromStream(stream, { - eagerRefreshThresholdMillis: 100, - }); - // Ensure that the correct bits were pulled from the stream. - const rc = result as UserRefreshClient; - assert.strictEqual(refreshJSON.client_id, rc._clientId); - assert.strictEqual(refreshJSON.client_secret, rc._clientSecret); - assert.strictEqual(refreshJSON.refresh_token, rc._refreshToken); - assert.strictEqual(100, rc.eagerRefreshThresholdMillis); - }); + it('should read another stream and create a UserRefreshClient with eager refresh', async () => { + const stream = fs.createReadStream('./test/fixtures/refresh.json'); + const auth = new GoogleAuth(); + const result = await auth.fromStream(stream, { + eagerRefreshThresholdMillis: 100, + }); + // Ensure that the correct bits were pulled from the stream. + const rc = result as UserRefreshClient; + assert.strictEqual(refreshJSON.client_id, rc._clientId); + assert.strictEqual(refreshJSON.client_secret, rc._clientSecret); + assert.strictEqual(refreshJSON.refresh_token, rc._refreshToken); + assert.strictEqual(100, rc.eagerRefreshThresholdMillis); + }); - it('getApplicationCredentialsFromFilePath should not error on valid symlink', async () => { - if (isWindows) { - // git does not create symlinks on Windows - return; - } - await auth._getApplicationCredentialsFromFilePath( - './test/fixtures/goodlink' - ); - }); + it('getApplicationCredentialsFromFilePath should not error on valid symlink', async () => { + if (isWindows) { + // git does not create symlinks on Windows + return; + } + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/goodlink' + ); + }); - it('getApplicationCredentialsFromFilePath should error on invalid symlink', async () => { - await assertRejects( - auth._getApplicationCredentialsFromFilePath('./test/fixtures/badlink') - ); - }); + it('getApplicationCredentialsFromFilePath should error on invalid symlink', async () => { + await assertRejects( + auth._getApplicationCredentialsFromFilePath('./test/fixtures/badlink') + ); + }); - it('getApplicationCredentialsFromFilePath should error on valid link to invalid data', async () => { - if (isWindows) { - // git does not create symlinks on Windows - return; - } - await assertRejects( - auth._getApplicationCredentialsFromFilePath('./test/fixtures/emptylink') - ); - }); + it('getApplicationCredentialsFromFilePath should error on valid link to invalid data', async () => { + if (isWindows) { + // git does not create symlinks on Windows + return; + } + await assertRejects( + auth._getApplicationCredentialsFromFilePath('./test/fixtures/emptylink') + ); + }); - it('getApplicationCredentialsFromFilePath should error on null file path', async () => { - try { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - await (auth as any)._getApplicationCredentialsFromFilePath(null); - } catch (e) { - return; - } - assert.fail('failed to throw'); - }); + it('getApplicationCredentialsFromFilePath should error on null file path', async () => { + try { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + await (auth as any)._getApplicationCredentialsFromFilePath(null); + } catch (e) { + return; + } + assert.fail('failed to throw'); + }); - it('getApplicationCredentialsFromFilePath should error on empty file path', async () => { - try { - await auth._getApplicationCredentialsFromFilePath(''); - } catch (e) { - return; - } - assert.fail('failed to throw'); - }); + it('getApplicationCredentialsFromFilePath should error on empty file path', async () => { + try { + await auth._getApplicationCredentialsFromFilePath(''); + } catch (e) { + return; + } + assert.fail('failed to throw'); + }); - it('getApplicationCredentialsFromFilePath should error on non-string file path', async () => { - try { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - await auth._getApplicationCredentialsFromFilePath(2 as any); - } catch (e) { - return; - } - assert.fail('failed to throw'); - }); + it('getApplicationCredentialsFromFilePath should error on non-string file path', async () => { + try { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + await auth._getApplicationCredentialsFromFilePath(2 as any); + } catch (e) { + return; + } + assert.fail('failed to throw'); + }); - it('getApplicationCredentialsFromFilePath should error on invalid file path', async () => { - try { - await auth._getApplicationCredentialsFromFilePath( - './nonexistantfile.json' + it('getApplicationCredentialsFromFilePath should error on invalid file path', async () => { + try { + await auth._getApplicationCredentialsFromFilePath( + './nonexistantfile.json' + ); + } catch (e) { + return; + } + assert.fail('failed to throw'); + }); + + it('getApplicationCredentialsFromFilePath should error on directory', async () => { + // Make sure that the following path actually does point to a directory. + const directory = './test/fixtures'; + await assertRejects( + auth._getApplicationCredentialsFromFilePath(directory) ); - } catch (e) { - return; - } - assert.fail('failed to throw'); - }); + }); - it('getApplicationCredentialsFromFilePath should error on directory', async () => { - // Make sure that the following path actually does point to a directory. - const directory = './test/fixtures'; - await assertRejects(auth._getApplicationCredentialsFromFilePath(directory)); - }); + it('getApplicationCredentialsFromFilePath should handle errors thrown from createReadStream', async () => { + await assertRejects( + auth._getApplicationCredentialsFromFilePath('./does/not/exist.json'), + /ENOENT: no such file or directory/ + ); + }); - it('getApplicationCredentialsFromFilePath should handle errors thrown from createReadStream', async () => { - await assertRejects( - auth._getApplicationCredentialsFromFilePath('./does/not/exist.json'), - /ENOENT: no such file or directory/ - ); - }); + it('getApplicationCredentialsFromFilePath should handle errors thrown from fromStream', async () => { + sandbox.stub(auth, 'fromStream').throws('🤮'); + await assertRejects( + auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json' + ), + /🤮/ + ); + }); - it('getApplicationCredentialsFromFilePath should handle errors thrown from fromStream', async () => { - sandbox.stub(auth, 'fromStream').throws('🤮'); - await assertRejects( - auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json' - ), - /🤮/ - ); - }); + it('getApplicationCredentialsFromFilePath should handle errors passed from fromStream', async () => { + // Set up a mock to return an error from the fromStream method. + sandbox.stub(auth, 'fromStream').throws('🤮'); + await assertRejects( + auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json' + ), + /🤮/ + ); + }); - it('getApplicationCredentialsFromFilePath should handle errors passed from fromStream', async () => { - // Set up a mock to return an error from the fromStream method. - sandbox.stub(auth, 'fromStream').throws('🤮'); - await assertRejects( - auth._getApplicationCredentialsFromFilePath( + it('getApplicationCredentialsFromFilePath should correctly read the file and create a valid JWT', async () => { + const result = await auth._getApplicationCredentialsFromFilePath( './test/fixtures/private.json' - ), - /🤮/ - ); - }); + ); + assert(result); + const jwt = result as JWT; + assert.strictEqual(privateJSON.private_key, jwt.key); + assert.strictEqual(privateJSON.client_email, jwt.email); + assert.strictEqual(undefined, jwt.keyFile); + assert.strictEqual(undefined, jwt.subject); + assert.strictEqual(undefined, jwt.scope); + }); - it('getApplicationCredentialsFromFilePath should correctly read the file and create a valid JWT', async () => { - const result = await auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json' - ); - assert(result); - const jwt = result as JWT; - assert.strictEqual(privateJSON.private_key, jwt.key); - assert.strictEqual(privateJSON.client_email, jwt.email); - assert.strictEqual(undefined, jwt.keyFile); - assert.strictEqual(undefined, jwt.subject); - assert.strictEqual(undefined, jwt.scope); - }); + it('getApplicationCredentialsFromFilePath should correctly read the file and create a valid JWT with eager refresh', async () => { + const result = await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json', + {eagerRefreshThresholdMillis: 7000} + ); + assert(result); + const jwt = result as JWT; + assert.strictEqual(privateJSON.private_key, jwt.key); + assert.strictEqual(privateJSON.client_email, jwt.email); + assert.strictEqual(undefined, jwt.keyFile); + assert.strictEqual(undefined, jwt.subject); + assert.strictEqual(undefined, jwt.scope); + assert.strictEqual(7000, jwt.eagerRefreshThresholdMillis); + }); - it('getApplicationCredentialsFromFilePath should correctly read the file and create a valid JWT with eager refresh', async () => { - const result = await auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json', - {eagerRefreshThresholdMillis: 7000} - ); - assert(result); - const jwt = result as JWT; - assert.strictEqual(privateJSON.private_key, jwt.key); - assert.strictEqual(privateJSON.client_email, jwt.email); - assert.strictEqual(undefined, jwt.keyFile); - assert.strictEqual(undefined, jwt.subject); - assert.strictEqual(undefined, jwt.scope); - assert.strictEqual(7000, jwt.eagerRefreshThresholdMillis); - }); + it('tryGetApplicationCredentialsFromEnvironmentVariable should return null when env const is not set', async () => { + // Set up a mock to return a null path string. + mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS'); + const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + assert.strictEqual(client, null); + }); - it('tryGetApplicationCredentialsFromEnvironmentVariable should return null when env const is not set', async () => { - // Set up a mock to return a null path string. - mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS'); - const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); - assert.strictEqual(client, null); - }); + it('tryGetApplicationCredentialsFromEnvironmentVariable should return null when env const is empty string', async () => { + // Set up a mock to return an empty path string. + const stub = mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS'); + const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + assert.strictEqual(client, null); + }); - it('tryGetApplicationCredentialsFromEnvironmentVariable should return null when env const is empty string', async () => { - // Set up a mock to return an empty path string. - const stub = mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS'); - const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); - assert.strictEqual(client, null); - }); + it('tryGetApplicationCredentialsFromEnvironmentVariable should handle invalid environment variable', async () => { + // Set up a mock to return a path to an invalid file. + mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS', './nonexistantfile.json'); + try { + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + } catch (e) { + return; + } + assert.fail('failed to throw'); + }); - it('tryGetApplicationCredentialsFromEnvironmentVariable should handle invalid environment variable', async () => { - // Set up a mock to return a path to an invalid file. - mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS', './nonexistantfile.json'); - try { - await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); - } catch (e) { - return; - } - assert.fail('failed to throw'); - }); + it('tryGetApplicationCredentialsFromEnvironmentVariable should handle valid environment variable', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); + const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + const jwt = result as JWT; + assert.strictEqual(privateJSON.private_key, jwt.key); + assert.strictEqual(privateJSON.client_email, jwt.email); + assert.strictEqual(undefined, jwt.keyFile); + assert.strictEqual(undefined, jwt.subject); + assert.strictEqual(undefined, jwt.scope); + }); - it('tryGetApplicationCredentialsFromEnvironmentVariable should handle valid environment variable', async () => { - // Set up a mock to return path to a valid credentials file. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' - ); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); - const jwt = result as JWT; - assert.strictEqual(privateJSON.private_key, jwt.key); - assert.strictEqual(privateJSON.client_email, jwt.email); - assert.strictEqual(undefined, jwt.keyFile); - assert.strictEqual(undefined, jwt.subject); - assert.strictEqual(undefined, jwt.scope); - }); + it('tryGetApplicationCredentialsFromEnvironmentVariable should handle valid environment variable when there is eager refresh set', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); + const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable( + {eagerRefreshThresholdMillis: 60 * 60 * 1000} + ); + const jwt = result as JWT; + assert.strictEqual(privateJSON.private_key, jwt.key); + assert.strictEqual(privateJSON.client_email, jwt.email); + assert.strictEqual(undefined, jwt.keyFile); + assert.strictEqual(undefined, jwt.subject); + assert.strictEqual(undefined, jwt.scope); + assert.strictEqual(60 * 60 * 1000, jwt.eagerRefreshThresholdMillis); + }); - it('tryGetApplicationCredentialsFromEnvironmentVariable should handle valid environment variable when there is eager refresh set', async () => { - // Set up a mock to return path to a valid credentials file. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' - ); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable( - {eagerRefreshThresholdMillis: 60 * 60 * 1000} - ); - const jwt = result as JWT; - assert.strictEqual(privateJSON.private_key, jwt.key); - assert.strictEqual(privateJSON.client_email, jwt.email); - assert.strictEqual(undefined, jwt.keyFile); - assert.strictEqual(undefined, jwt.subject); - assert.strictEqual(undefined, jwt.scope); - assert.strictEqual(60 * 60 * 1000, jwt.eagerRefreshThresholdMillis); - }); + it('_tryGetApplicationCredentialsFromWellKnownFile should build the correct directory for Windows', async () => { + mockWindows(); + mockWindowsWellKnownFile(); + const result = (await auth._tryGetApplicationCredentialsFromWellKnownFile()) as JWT; + assert.ok(result); + assert.strictEqual(result.email, private2JSON.client_email); + }); - it('_tryGetApplicationCredentialsFromWellKnownFile should build the correct directory for Windows', async () => { - mockWindows(); - mockWindowsWellKnownFile(); - const result = (await auth._tryGetApplicationCredentialsFromWellKnownFile()) as JWT; - assert.ok(result); - assert.strictEqual(result.email, private2JSON.client_email); - }); + it('_tryGetApplicationCredentialsFromWellKnownFile should build the correct directory for non-Windows', async () => { + mockLinuxWellKnownFile(); + const client = (await auth._tryGetApplicationCredentialsFromWellKnownFile()) as JWT; + assert.strictEqual(client.email, private2JSON.client_email); + }); - it('_tryGetApplicationCredentialsFromWellKnownFile should build the correct directory for non-Windows', async () => { - mockLinuxWellKnownFile(); - const client = (await auth._tryGetApplicationCredentialsFromWellKnownFile()) as JWT; - assert.strictEqual(client.email, private2JSON.client_email); - }); + it('_tryGetApplicationCredentialsFromWellKnownFile should fail on Windows when APPDATA is not defined', async () => { + mockWindows(); + mockEnvVar('APPDATA'); + mockWindowsWellKnownFile(); + const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.strictEqual(null, result); + }); - it('_tryGetApplicationCredentialsFromWellKnownFile should fail on Windows when APPDATA is not defined', async () => { - mockWindows(); - mockEnvVar('APPDATA'); - mockWindowsWellKnownFile(); - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); - assert.strictEqual(null, result); - }); + it('_tryGetApplicationCredentialsFromWellKnownFile should fail on non-Windows when HOME is not defined', async () => { + mockEnvVar('HOME'); + mockLinuxWellKnownFile(); + const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.strictEqual(null, result); + }); - it('_tryGetApplicationCredentialsFromWellKnownFile should fail on non-Windows when HOME is not defined', async () => { - mockEnvVar('HOME'); - mockLinuxWellKnownFile(); - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); - assert.strictEqual(null, result); - }); + it('_tryGetApplicationCredentialsFromWellKnownFile should fail on Windows when file does not exist', async () => { + mockWindows(); + const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.strictEqual(null, result); + }); - it('_tryGetApplicationCredentialsFromWellKnownFile should fail on Windows when file does not exist', async () => { - mockWindows(); - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); - assert.strictEqual(null, result); - }); + it('_tryGetApplicationCredentialsFromWellKnownFile should fail on non-Windows when file does not exist', async () => { + const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.strictEqual(null, result); + }); - it('_tryGetApplicationCredentialsFromWellKnownFile should fail on non-Windows when file does not exist', async () => { - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); - assert.strictEqual(null, result); - }); + it('_tryGetApplicationCredentialsFromWellKnownFile should pass along a failure on Windows', async () => { + mockWindows(); + mockWindowsWellKnownFile(); + sandbox + .stub(auth, '_getApplicationCredentialsFromFilePath') + .rejects('🤮'); + await assertRejects( + auth._tryGetApplicationCredentialsFromWellKnownFile(), + /🤮/ + ); + }); - it('_tryGetApplicationCredentialsFromWellKnownFile should pass along a failure on Windows', async () => { - mockWindows(); - mockWindowsWellKnownFile(); - sandbox.stub(auth, '_getApplicationCredentialsFromFilePath').rejects('🤮'); - await assertRejects( - auth._tryGetApplicationCredentialsFromWellKnownFile(), - /🤮/ - ); - }); + it('_tryGetApplicationCredentialsFromWellKnownFile should pass along a failure on non-Windows', async () => { + mockLinuxWellKnownFile(); + sandbox + .stub(auth, '_getApplicationCredentialsFromFilePath') + .rejects('🤮'); + await assertRejects( + auth._tryGetApplicationCredentialsFromWellKnownFile(), + /🤮/ + ); + }); - it('_tryGetApplicationCredentialsFromWellKnownFile should pass along a failure on non-Windows', async () => { - mockLinuxWellKnownFile(); - sandbox.stub(auth, '_getApplicationCredentialsFromFilePath').rejects('🤮'); - await assertRejects( - auth._tryGetApplicationCredentialsFromWellKnownFile(), - /🤮/ - ); - }); + it('getProjectId should return a new projectId the first time and a cached projectId the second time', async () => { + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - it('getProjectId should return a new projectId the first time and a cached projectId the second time', async () => { - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + // Ask for credentials, the first time. + const projectIdPromise = auth.getProjectId(); + const projectId = await projectIdPromise; + assert.strictEqual(projectId, STUB_PROJECT); - // Ask for credentials, the first time. - const projectIdPromise = auth.getProjectId(); - const projectId = await projectIdPromise; - assert.strictEqual(projectId, STUB_PROJECT); + // Null out all the private functions that make this method work + // tslint:disable-next-line no-any + const anyd = auth as any; + anyd.getProductionProjectId = null; + anyd.getFileProjectId = null; + anyd.getDefaultServiceProjectId = null; + anyd.getGCEProjectId = null; - // Null out all the private functions that make this method work - // tslint:disable-next-line no-any - const anyd = auth as any; - anyd.getProductionProjectId = null; - anyd.getFileProjectId = null; - anyd.getDefaultServiceProjectId = null; - anyd.getGCEProjectId = null; + // Ask for projectId again, from the same auth instance. If it isn't + // cached, this will crash. + const projectId2 = await auth.getProjectId(); - // Ask for projectId again, from the same auth instance. If it isn't - // cached, this will crash. - const projectId2 = await auth.getProjectId(); + // Make sure we get the original cached projectId back + assert.strictEqual(STUB_PROJECT, projectId2); - // Make sure we get the original cached projectId back - assert.strictEqual(STUB_PROJECT, projectId2); + // Now create a second GoogleAuth instance, and ask for projectId. + // We should get a new projectId instance this time. + const auth2 = new GoogleAuth(); - // Now create a second GoogleAuth instance, and ask for projectId. - // We should get a new projectId instance this time. - const auth2 = new GoogleAuth(); + const getProjectIdPromise = auth2.getProjectId(); + assert.notStrictEqual(getProjectIdPromise, projectIdPromise); + }); - const getProjectIdPromise = auth2.getProjectId(); - assert.notStrictEqual(getProjectIdPromise, projectIdPromise); - }); + it('getProjectId should use GCLOUD_PROJECT environment variable when it is set', async () => { + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + const projectId = await auth.getProjectId(); + assert.strictEqual(projectId, STUB_PROJECT); + }); - it('getProjectId should use GCLOUD_PROJECT environment variable when it is set', async () => { - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - const projectId = await auth.getProjectId(); - assert.strictEqual(projectId, STUB_PROJECT); - }); + it('getProjectId should use `gcloud_project` environment variable when it is set', async () => { + process.env.gcloud_project = STUB_PROJECT; + const projectId = await auth.getProjectId(); + assert.strictEqual(projectId, STUB_PROJECT); + }); - it('getProjectId should use `gcloud_project` environment variable when it is set', async () => { - process.env.gcloud_project = STUB_PROJECT; - const projectId = await auth.getProjectId(); - assert.strictEqual(projectId, STUB_PROJECT); - }); + it('getProjectId should use GOOGLE_CLOUD_PROJECT environment variable when it is set', async () => { + process.env.GOOGLE_CLOUD_PROJECT = STUB_PROJECT; + const projectId = await auth.getProjectId(); + assert.strictEqual(projectId, STUB_PROJECT); + }); - it('getProjectId should use GOOGLE_CLOUD_PROJECT environment variable when it is set', async () => { - process.env.GOOGLE_CLOUD_PROJECT = STUB_PROJECT; - const projectId = await auth.getProjectId(); - assert.strictEqual(projectId, STUB_PROJECT); - }); + it('getProjectId should use `google_cloud_project` environment variable when it is set', async () => { + process.env['google_cloud_project'] = STUB_PROJECT; + const projectId = await auth.getProjectId(); + assert.strictEqual(projectId, STUB_PROJECT); + }); - it('getProjectId should use `google_cloud_project` environment variable when it is set', async () => { - process.env['google_cloud_project'] = STUB_PROJECT; - const projectId = await auth.getProjectId(); - assert.strictEqual(projectId, STUB_PROJECT); - }); + it('getProjectId should use `keyFilename` when it is available', async () => { + const auth = new GoogleAuth({ + keyFilename: './test/fixtures/private2.json', + }); + const projectId = await auth.getProjectId(); + assert.strictEqual(projectId, STUB_PROJECT); + }); - it('getProjectId should use `keyFilename` when it is available', async () => { - const auth = new GoogleAuth({keyFilename: './test/fixtures/private2.json'}); - const projectId = await auth.getProjectId(); - assert.strictEqual(projectId, STUB_PROJECT); - }); + it('getProjectId should use GOOGLE_APPLICATION_CREDENTIALS file when it is available', async () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = + './test/fixtures/private2.json'; + const projectId = await auth.getProjectId(); + assert.strictEqual(projectId, STUB_PROJECT); + }); - it('getProjectId should use GOOGLE_APPLICATION_CREDENTIALS file when it is available', async () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = - './test/fixtures/private2.json'; - const projectId = await auth.getProjectId(); - assert.strictEqual(projectId, STUB_PROJECT); - }); + it('getProjectId should use `google_application_credentials` file when it is available', async () => { + process.env['google_application_credentials'] = + './test/fixtures/private2.json'; + const projectId = await auth.getProjectId(); + assert.strictEqual(projectId, STUB_PROJECT); + }); - it('getProjectId should use `google_application_credentials` file when it is available', async () => { - process.env['google_application_credentials'] = - './test/fixtures/private2.json'; - const projectId = await auth.getProjectId(); - assert.strictEqual(projectId, STUB_PROJECT); - }); + it('getProjectId should prefer configured projectId', async () => { + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + mockEnvVar('GOOGLE_CLOUD_PROJECT', STUB_PROJECT); + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private2.json' + ); + const PROJECT_ID = 'configured-project-id-should-be-preferred'; + const auth = new GoogleAuth({projectId: PROJECT_ID}); + const projectId = await auth.getProjectId(); + assert.strictEqual(projectId, PROJECT_ID); + }); - it('getProjectId should prefer configured projectId', async () => { - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - mockEnvVar('GOOGLE_CLOUD_PROJECT', STUB_PROJECT); - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' - ); - const PROJECT_ID = 'configured-project-id-should-be-preferred'; - const auth = new GoogleAuth({projectId: PROJECT_ID}); - const projectId = await auth.getProjectId(); - assert.strictEqual(projectId, PROJECT_ID); - }); + it('getProjectId should use Cloud SDK when it is available and env vars are not set', async () => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is set up to point to private2.json + // * Running on GCE is set to true. + const stdout = JSON.stringify({ + configuration: {properties: {core: {project: STUB_PROJECT}}}, + }); - it('getProjectId should use Cloud SDK when it is available and env vars are not set', async () => { - // Set up the creds. - // * Environment variable is not set. - // * Well-known file is set up to point to private2.json - // * Running on GCE is set to true. - const stdout = JSON.stringify({ - configuration: {properties: {core: {project: STUB_PROJECT}}}, - }); - - ((child_process.exec as unknown) as sinon.SinonStub).restore(); - const stub = sandbox - .stub(child_process, 'exec') - .callsArgWith(1, null, stdout, null); - const projectId = await auth.getProjectId(); - assert(stub.calledOnce); - assert.strictEqual(projectId, STUB_PROJECT); - }); + ((child_process.exec as unknown) as sinon.SinonStub).restore(); + const stub = sandbox + .stub(child_process, 'exec') + .callsArgWith(1, null, stdout, null); + const projectId = await auth.getProjectId(); + assert(stub.calledOnce); + assert.strictEqual(projectId, STUB_PROJECT); + }); - it('getProjectId should use GCE when well-known file and env const are not set', async () => { - const scope = createGetProjectIdNock(STUB_PROJECT); - const projectId = await auth.getProjectId(); - const stub = (child_process.exec as unknown) as sinon.SinonStub; - stub.restore(); - assert(stub.calledOnce); - assert.strictEqual(projectId, STUB_PROJECT); - scope.done(); - }); + it('getProjectId should use GCE when well-known file and env const are not set', async () => { + const scope = createGetProjectIdNock(STUB_PROJECT); + const projectId = await auth.getProjectId(); + const stub = (child_process.exec as unknown) as sinon.SinonStub; + stub.restore(); + assert(stub.calledOnce); + assert.strictEqual(projectId, STUB_PROJECT); + scope.done(); + }); - it('getApplicationDefault should return a new credential the first time and a cached credential the second time', async () => { - // Create a function which will set up a GoogleAuth instance to match - // on an environment variable json file, but not on anything else. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' - ); - - // Ask for credentials, the first time. - const result = await auth.getApplicationDefault(); - assert.notStrictEqual(null, result); - - // Capture the returned credential. - const cachedCredential = result.credential; - - // Make sure our special test bit is not set yet, indicating that - // this is a new credentials instance. - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - assert.strictEqual(undefined, (cachedCredential as any).specialTestBit); - - // Now set the special test bit. - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (cachedCredential as any).specialTestBit = 'monkey'; - - // Ask for credentials again, from the same auth instance. We expect - // a cached instance this time. - const result2 = (await auth.getApplicationDefault()).credential; - assert.notStrictEqual(null, result2); - - // Make sure the special test bit is set on the credentials we got - // back, indicating that we got cached credentials. Also make sure - // the object instance is the same. - // Test verifies invalid parameter tests, which requires cast to - // any. - // tslint:disable-next-line no-any - assert.strictEqual('monkey', (result2 as any).specialTestBit); - assert.strictEqual(cachedCredential, result2); - - // Now create a second GoogleAuth instance, and ask for - // credentials. We should get a new credentials instance this time. - const auth2 = new GoogleAuth(); - const result3 = (await auth2.getApplicationDefault()).credential; - assert.notStrictEqual(null, result3); - - // Make sure we get a new (non-cached) credential instance back. - // Test verifies invalid parameter tests, which requires cast to - // any. - // tslint:disable-next-line no-any - assert.strictEqual(undefined, (result3 as any).specialTestBit); - assert.notStrictEqual(cachedCredential, result3); - }); + it('getApplicationDefault should return a new credential the first time and a cached credential the second time', async () => { + // Create a function which will set up a GoogleAuth instance to match + // on an environment variable json file, but not on anything else. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private2.json' + ); - it('getApplicationDefault should cache the credential when using GCE', async () => { - const scopes = [nockIsGCE(), createGetProjectIdNock()]; + // Ask for credentials, the first time. + const result = await auth.getApplicationDefault(); + assert.notStrictEqual(null, result); - // Ask for credentials, the first time. - const result = await auth.getApplicationDefault(); - scopes.forEach(x => x.done()); - assert.notStrictEqual(null, result); + // Capture the returned credential. + const cachedCredential = result.credential; - // Capture the returned credential. - const cachedCredential = result.credential; - // Ask for credentials again, from the same auth instance. We expect - // a cached instance this time. - const result2 = (await auth.getApplicationDefault()).credential; - assert.notStrictEqual(null, result2); + // Make sure our special test bit is not set yet, indicating that + // this is a new credentials instance. + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + assert.strictEqual(undefined, (cachedCredential as any).specialTestBit); - // Make sure it's the same object - assert.strictEqual(cachedCredential, result2); - }); + // Now set the special test bit. + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (cachedCredential as any).specialTestBit = 'monkey'; + + // Ask for credentials again, from the same auth instance. We expect + // a cached instance this time. + const result2 = (await auth.getApplicationDefault()).credential; + assert.notStrictEqual(null, result2); + + // Make sure the special test bit is set on the credentials we got + // back, indicating that we got cached credentials. Also make sure + // the object instance is the same. + // Test verifies invalid parameter tests, which requires cast to + // any. + // tslint:disable-next-line no-any + assert.strictEqual('monkey', (result2 as any).specialTestBit); + assert.strictEqual(cachedCredential, result2); + + // Now create a second GoogleAuth instance, and ask for + // credentials. We should get a new credentials instance this time. + const auth2 = new GoogleAuth(); + const result3 = (await auth2.getApplicationDefault()).credential; + assert.notStrictEqual(null, result3); + + // Make sure we get a new (non-cached) credential instance back. + // Test verifies invalid parameter tests, which requires cast to + // any. + // tslint:disable-next-line no-any + assert.strictEqual(undefined, (result3 as any).specialTestBit); + assert.notStrictEqual(cachedCredential, result3); + }); - it('getApplicationDefault should use environment variable when it is set', async () => { - // Set up the creds. - // * Environment variable is set up to point to private.json - // * Well-known file is set up to point to private2.json - // * Running on GCE is set to true. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' - ); - mockWindows(); - mockWindowsWellKnownFile(); - nockIsGCE(); - - const res = await auth.getApplicationDefault(); - const client = res.credential as JWT; - assert.strictEqual(private2JSON.private_key, client.key); - assert.strictEqual(private2JSON.client_email, client.email); - assert.strictEqual(undefined, client.keyFile); - assert.strictEqual(undefined, client.subject); - assert.strictEqual(undefined, client.scope); - }); + it('getApplicationDefault should cache the credential when using GCE', async () => { + const scopes = [nockIsGCE(), createGetProjectIdNock()]; - it('should use well-known file when it is available and env const is not set', async () => { - // Set up the creds. - // * Environment variable is not set. - // * Well-known file is set up to point to private2.json - mockLinuxWellKnownFile(); - - const res = await auth.getApplicationDefault(); - const client = res.credential as JWT; - assert.strictEqual(private2JSON.private_key, client.key); - assert.strictEqual(private2JSON.client_email, client.email); - assert.strictEqual(undefined, client.keyFile); - assert.strictEqual(undefined, client.subject); - assert.strictEqual(undefined, client.scope); - }); + // Ask for credentials, the first time. + const result = await auth.getApplicationDefault(); + scopes.forEach(x => x.done()); + assert.notStrictEqual(null, result); - it('getApplicationDefault should use GCE when well-known file and env const are not set', async () => { - // Set up the creds. - // * Environment variable is not set. - // * Well-known file is not set. - // * Running on GCE is set to true. - const scopes = [nockIsGCE(), createGetProjectIdNock()]; - const res = await auth.getApplicationDefault(); - scopes.forEach(x => x.done()); - // This indicates that we got a ComputeClient instance back, rather than - // a JWTClient. - assert.strictEqual( - 'compute-placeholder', - res.credential.credentials.refresh_token - ); - }); + // Capture the returned credential. + const cachedCredential = result.credential; + // Ask for credentials again, from the same auth instance. We expect + // a cached instance this time. + const result2 = (await auth.getApplicationDefault()).credential; + assert.notStrictEqual(null, result2); - it('getApplicationDefault should report GCE error when checking for GCE fails', async () => { - // Set up the creds. - // * Environment variable is not set. - // * Well-known file is not set. - // * Running on GCE is set to true. - mockWindows(); - sandbox.stub(auth, '_checkIsGCE').rejects('🤮'); - await assertRejects( - auth.getApplicationDefault(), - /Unexpected error determining execution environment/ - ); - }); + // Make sure it's the same object + assert.strictEqual(cachedCredential, result2); + }); - it('getApplicationDefault should also get project ID', async () => { - // Set up the creds. - // * Environment variable is set up to point to private.json - // * Well-known file is set up to point to private2.json - // * Running on GCE is set to true. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' - ); - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - mockWindows(); - mockGCE(); - mockWindowsWellKnownFile(); - - const res = await auth.getApplicationDefault(); - const client = res.credential as JWT; - assert.strictEqual(private2JSON.private_key, client.key); - assert.strictEqual(private2JSON.client_email, client.email); - assert.strictEqual(res.projectId, STUB_PROJECT); - assert.strictEqual(undefined, client.keyFile); - assert.strictEqual(undefined, client.subject); - assert.strictEqual(undefined, client.scope); - }); + it('getApplicationDefault should use environment variable when it is set', async () => { + // Set up the creds. + // * Environment variable is set up to point to private.json + // * Well-known file is set up to point to private2.json + // * Running on GCE is set to true. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private2.json' + ); + mockWindows(); + mockWindowsWellKnownFile(); + nockIsGCE(); + + const res = await auth.getApplicationDefault(); + const client = res.credential as JWT; + assert.strictEqual(private2JSON.private_key, client.key); + assert.strictEqual(private2JSON.client_email, client.email); + assert.strictEqual(undefined, client.keyFile); + assert.strictEqual(undefined, client.subject); + assert.strictEqual(undefined, client.scope); + }); - it('_checkIsGCE should set the _isGCE flag when running on GCE', async () => { - assert.notStrictEqual(true, auth.isGCE); - const scope = nockIsGCE(); - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - scope.done(); - }); + it('should use well-known file when it is available and env const is not set', async () => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is set up to point to private2.json + mockLinuxWellKnownFile(); + + const res = await auth.getApplicationDefault(); + const client = res.credential as JWT; + assert.strictEqual(private2JSON.private_key, client.key); + assert.strictEqual(private2JSON.client_email, client.email); + assert.strictEqual(undefined, client.keyFile); + assert.strictEqual(undefined, client.subject); + assert.strictEqual(undefined, client.scope); + }); - it('_checkIsGCE should not set the _isGCE flag when not running on GCE', async () => { - const scope = nockNotGCE(); - assert.notStrictEqual(true, auth.isGCE); - await auth._checkIsGCE(); - assert.strictEqual(false, auth.isGCE); - scope.done(); - }); + it('getApplicationDefault should use GCE when well-known file and env const are not set', async () => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is not set. + // * Running on GCE is set to true. + const scopes = [nockIsGCE(), createGetProjectIdNock()]; + const res = await auth.getApplicationDefault(); + scopes.forEach(x => x.done()); + // This indicates that we got a ComputeClient instance back, rather than + // a JWTClient. + assert.strictEqual( + 'compute-placeholder', + res.credential.credentials.refresh_token + ); + }); - it('_checkIsGCE should retry the check for isGCE on transient http errors', async () => { - assert.notStrictEqual(true, auth.isGCE); - // the first request will fail, the second one will succeed - const scopes = [nock500GCE(), nockIsGCE()]; - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - scopes.forEach(s => s.done()); - }); + it('getApplicationDefault should report GCE error when checking for GCE fails', async () => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is not set. + // * Running on GCE is set to true. + mockWindows(); + sandbox.stub(auth, '_checkIsGCE').rejects('🤮'); + await assertRejects( + auth.getApplicationDefault(), + /Unexpected error determining execution environment/ + ); + }); - it('_checkIsGCE should throw on unexpected errors', async () => { - assert.notStrictEqual(true, auth.isGCE); - const scope = nock404GCE(); - await assertRejects(auth._checkIsGCE()); - assert.strictEqual(undefined, auth.isGCE); - scope.done(); - }); + it('getApplicationDefault should also get project ID', async () => { + // Set up the creds. + // * Environment variable is set up to point to private.json + // * Well-known file is set up to point to private2.json + // * Running on GCE is set to true. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private2.json' + ); + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + mockWindows(); + mockGCE(); + mockWindowsWellKnownFile(); + + const res = await auth.getApplicationDefault(); + const client = res.credential as JWT; + assert.strictEqual(private2JSON.private_key, client.key); + assert.strictEqual(private2JSON.client_email, client.email); + assert.strictEqual(res.projectId, STUB_PROJECT); + assert.strictEqual(undefined, client.keyFile); + assert.strictEqual(undefined, client.subject); + assert.strictEqual(undefined, client.scope); + }); - it('_checkIsGCE should not retry the check for isGCE if it fails with an ENOTFOUND', async () => { - assert.notStrictEqual(true, auth.isGCE); - const scope = nockNotGCE(); - await auth._checkIsGCE(); - assert.strictEqual(false, auth.isGCE); - scope.done(); - }); + it('_checkIsGCE should set the _isGCE flag when running on GCE', async () => { + assert.notStrictEqual(true, auth.isGCE); + const scope = nockIsGCE(); + await auth._checkIsGCE(); + assert.strictEqual(true, auth.isGCE); + scope.done(); + }); - it('_checkIsGCE does not execute the second time when running on GCE', async () => { - // This test relies on the nock mock only getting called once. - assert.notStrictEqual(true, auth.isGCE); - const scope = nockIsGCE(); - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - scope.done(); - }); + it('_checkIsGCE should not set the _isGCE flag when not running on GCE', async () => { + const scope = nockNotGCE(); + assert.notStrictEqual(true, auth.isGCE); + await auth._checkIsGCE(); + assert.strictEqual(false, auth.isGCE); + scope.done(); + }); - it('_checkIsGCE does not execute the second time when not running on GCE', async () => { - assert.notStrictEqual(true, auth.isGCE); - const scope = nockNotGCE(); - await auth._checkIsGCE(); - assert.strictEqual(false, auth.isGCE); - await auth._checkIsGCE(); - assert.strictEqual(false, auth.isGCE); - scope.done(); - }); + it('_checkIsGCE should retry the check for isGCE on transient http errors', async () => { + assert.notStrictEqual(true, auth.isGCE); + // the first request will fail, the second one will succeed + const scopes = [nock500GCE(), nockIsGCE()]; + await auth._checkIsGCE(); + assert.strictEqual(true, auth.isGCE); + scopes.forEach(s => s.done()); + }); - it('getCredentials should get metadata from the server when running on GCE', async () => { - const response = { - default: { - email: 'test-creds@test-creds.iam.gserviceaccount.com', - private_key: null, - }, - }; - const scopes = [ - nockIsGCE(), - createGetProjectIdNock(), - nock(host) - .get(svcAccountPath) - .reply(200, response, HEADERS), - ]; - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - const body = await auth.getCredentials(); - assert.ok(body); - assert.strictEqual( - body.client_email, - 'test-creds@test-creds.iam.gserviceaccount.com' - ); - assert.strictEqual(body.private_key, undefined); - scopes.forEach(s => s.done()); - }); + it('_checkIsGCE should throw on unexpected errors', async () => { + assert.notStrictEqual(true, auth.isGCE); + const scope = nock500GCE(); + await assertRejects(auth._checkIsGCE()); + assert.strictEqual(undefined, auth.isGCE); + scope.done(); + }); - it('getCredentials should error if metadata server is not reachable', async () => { - const scopes = [ - nockIsGCE(), - createGetProjectIdNock(), - nock(HOST_ADDRESS) - .get(svcAccountPath) - .reply(404), - ]; - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - await assertRejects( - auth.getCredentials(), - /Unsuccessful response status code. Request failed with status code 404/ - ); - scopes.forEach(s => s.done()); - }); + it('_checkIsGCE should not retry the check for isGCE if it fails with an ENOTFOUND', async () => { + assert.notStrictEqual(true, auth.isGCE); + const scope = nockNotGCE(); + await auth._checkIsGCE(); + assert.strictEqual(false, auth.isGCE); + scope.done(); + }); - it('getCredentials should error if body is empty', async () => { - const scopes = [ - nockIsGCE(), - createGetProjectIdNock(), - nock(HOST_ADDRESS) - .get(svcAccountPath) - .reply(200, {}), - ]; - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - await assertRejects( - auth.getCredentials(), - /Invalid response from metadata service: incorrect Metadata-Flavor header./ - ); - scopes.forEach(s => s.done()); - }); + it('_checkIsGCE does not execute the second time when running on GCE', async () => { + // This test relies on the nock mock only getting called once. + assert.notStrictEqual(true, auth.isGCE); + const scope = nockIsGCE(); + await auth._checkIsGCE(); + assert.strictEqual(true, auth.isGCE); + await auth._checkIsGCE(); + assert.strictEqual(true, auth.isGCE); + scope.done(); + }); - it('getCredentials should handle valid environment variable', async () => { - // Set up a mock to return path to a valid credentials file. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' - ); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); - assert(result); - const jwt = result as JWT; - const body = await auth.getCredentials(); - assert.notStrictEqual(null, body); - assert.strictEqual(jwt.email, body.client_email); - assert.strictEqual(jwt.key, body.private_key); - }); + it('_checkIsGCE does not execute the second time when not running on GCE', async () => { + assert.notStrictEqual(true, auth.isGCE); + const scope = nockNotGCE(); + await auth._checkIsGCE(); + assert.strictEqual(false, auth.isGCE); + await auth._checkIsGCE(); + assert.strictEqual(false, auth.isGCE); + scope.done(); + }); - it('getCredentials should call getClient to load credentials', async () => { - // Set up a mock to return path to a valid credentials file. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' - ); + it('getCredentials should get metadata from the server when running on GCE', async () => { + const response = { + default: { + email: 'test-creds@test-creds.iam.gserviceaccount.com', + private_key: null, + }, + }; + const scopes = [ + nockIsGCE(), + createGetProjectIdNock(), + nock(host) + .get(svcAccountPath) + .reply(200, response, HEADERS), + ]; + await auth._checkIsGCE(); + assert.strictEqual(true, auth.isGCE); + const body = await auth.getCredentials(); + assert.ok(body); + assert.strictEqual( + body.client_email, + 'test-creds@test-creds.iam.gserviceaccount.com' + ); + assert.strictEqual(body.private_key, undefined); + scopes.forEach(s => s.done()); + }); - const spy = sinon.spy(auth, 'getClient'); - const body = await auth.getCredentials(); + it('getCredentials should error if metadata server is not reachable', async () => { + const scopes = [ + nockIsGCE(), + createGetProjectIdNock(), + nock(HOST_ADDRESS) + .get(svcAccountPath) + .reply(404), + ]; + await auth._checkIsGCE(); + assert.strictEqual(true, auth.isGCE); + await assertRejects( + auth.getCredentials(), + /Unsuccessful response status code. Request failed with status code 404/ + ); + scopes.forEach(s => s.done()); + }); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); - if (!(result instanceof JWT)) { - throw new assert.AssertionError({ - message: 'Credentials are not a JWT object', - }); - } + it('getCredentials should error if body is empty', async () => { + const scopes = [ + nockIsGCE(), + createGetProjectIdNock(), + nock(HOST_ADDRESS) + .get(svcAccountPath) + .reply(200, {}), + ]; + await auth._checkIsGCE(); + assert.strictEqual(true, auth.isGCE); + await assertRejects( + auth.getCredentials(), + /Invalid response from metadata service: incorrect Metadata-Flavor header./ + ); + scopes.forEach(s => s.done()); + }); - assert.notStrictEqual(null, body); - assert(spy.calledOnce); - assert.strictEqual(result.email, body!.client_email); - assert.strictEqual(result.key, body!.private_key); - }); + it('getCredentials should handle valid environment variable', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); + const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + assert(result); + const jwt = result as JWT; + const body = await auth.getCredentials(); + assert.notStrictEqual(null, body); + assert.strictEqual(jwt.email, body.client_email); + assert.strictEqual(jwt.key, body.private_key); + }); - it('getCredentials should handle valid file path', async () => { - // Set up a mock to return path to a valid credentials file. - mockWindows(); - auth._checkIsGCE = () => Promise.resolve(true); - mockWindowsWellKnownFile(); - const result = await auth.getApplicationDefault(); - assert(result); - const jwt = result.credential as JWT; - const body = await auth.getCredentials(); - assert.notStrictEqual(null, body); - assert.strictEqual(jwt.email, body!.client_email); - assert.strictEqual(jwt.key, body!.private_key); - }); + it('getCredentials should call getClient to load credentials', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); - it('getCredentials should return error when env const is not set', async () => { - // Set up a mock to return a null path string - const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); - assert.strictEqual(null, client); - await assertRejects(auth.getCredentials()); - }); + const spy = sinon.spy(auth, 'getClient'); + const body = await auth.getCredentials(); - it('should use jsonContent if available', async () => { - const json = createJwtJSON(); - const auth = new GoogleAuth({credentials: json}); - // We know this returned a cached result if a nock scope isn't required - const body = await auth.getCredentials(); - assert.notStrictEqual(body, null); - assert.strictEqual(body!.client_email, 'hello@youarecool.com'); - }); + const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + if (!(result instanceof JWT)) { + throw new assert.AssertionError({ + message: 'Credentials are not a JWT object', + }); + } - it('should accept keyFilename to get a client', async () => { - const auth = new GoogleAuth({keyFilename: './test/fixtures/private.json'}); - const client = (await auth.getClient()) as JWT; - assert.strictEqual(client.email, 'hello@youarecool.com'); - }); + assert.notStrictEqual(null, body); + assert(spy.calledOnce); + assert.strictEqual(result.email, body!.client_email); + assert.strictEqual(result.key, body!.private_key); + }); - it('should error when invalid keyFilename passed to getClient', async () => { - const auth = new GoogleAuth({keyFilename: './funky/fresh.json'}); - await assertRejects(auth.getClient(), /ENOENT: no such file or directory/); - }); + it('getCredentials should handle valid file path', async () => { + // Set up a mock to return path to a valid credentials file. + mockWindows(); + auth._checkIsGCE = () => Promise.resolve(true); + mockWindowsWellKnownFile(); + const result = await auth.getApplicationDefault(); + assert(result); + const jwt = result.credential as JWT; + const body = await auth.getCredentials(); + assert.notStrictEqual(null, body); + assert.strictEqual(jwt.email, body!.client_email); + assert.strictEqual(jwt.key, body!.private_key); + }); - it('should accept credentials to get a client', async () => { - const auth = new GoogleAuth({credentials: privateJSON}); - const client = (await auth.getClient()) as JWT; - assert.strictEqual(client.email, 'hello@youarecool.com'); - }); + it('getCredentials should return error when env const is not set', async () => { + // Set up a mock to return a null path string + const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + assert.strictEqual(null, client); + await assertRejects(auth.getCredentials()); + }); - it('should prefer credentials over keyFilename', async () => { - const credentials: CredentialBody = Object.assign({}, privateJSON, { - client_email: 'hello@butiamcooler.com', + it('should use jsonContent if available', async () => { + const json = createJwtJSON(); + const auth = new GoogleAuth({credentials: json}); + // We know this returned a cached result if a nock scope isn't required + const body = await auth.getCredentials(); + assert.notStrictEqual(body, null); + assert.strictEqual(body!.client_email, 'hello@youarecool.com'); }); - const auth = new GoogleAuth({ - credentials, - keyFilename: './test/fixtures/private.json', + + it('should accept keyFilename to get a client', async () => { + const auth = new GoogleAuth({ + keyFilename: './test/fixtures/private.json', + }); + const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.email, 'hello@youarecool.com'); }); - const client = (await auth.getClient()) as JWT; - assert.strictEqual(client.email, credentials.client_email); - }); - it('should allow passing scopes to get a client', async () => { - const scopes = ['http://examples.com/is/a/scope']; - const keyFilename = './test/fixtures/private.json'; - const auth = new GoogleAuth({scopes, keyFilename}); - const client = (await auth.getClient()) as JWT; - assert.strictEqual(client.scopes, scopes); - }); + it('should error when invalid keyFilename passed to getClient', async () => { + const auth = new GoogleAuth({keyFilename: './funky/fresh.json'}); + await assertRejects( + auth.getClient(), + /ENOENT: no such file or directory/ + ); + }); - it('should allow passing a scope to get a client', async () => { - const scopes = 'http://examples.com/is/a/scope'; - const keyFilename = './test/fixtures/private.json'; - const auth = new GoogleAuth({scopes, keyFilename}); - const client = (await auth.getClient()) as JWT; - assert.strictEqual(client.scopes, scopes); - }); + it('should accept credentials to get a client', async () => { + const auth = new GoogleAuth({credentials: privateJSON}); + const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.email, 'hello@youarecool.com'); + }); - it('should allow passing a scope to get a Compute client', async () => { - const scopes = ['http://examples.com/is/a/scope']; - const nockScopes = [nockIsGCE(), createGetProjectIdNock()]; - const auth = new GoogleAuth({scopes}); - const client = (await auth.getClient()) as Compute; - assert.strictEqual(client.scopes, scopes); - nockScopes.forEach(x => x.done()); - }); + it('should prefer credentials over keyFilename', async () => { + const credentials: CredentialBody = Object.assign({}, privateJSON, { + client_email: 'hello@butiamcooler.com', + }); + const auth = new GoogleAuth({ + credentials, + keyFilename: './test/fixtures/private.json', + }); + const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.email, credentials.client_email); + }); - it('should get an access token', async () => { - const {auth, scopes} = mockGCE(); - scopes.push(createGetProjectIdNock()); - const token = await auth.getAccessToken(); - scopes.forEach(s => s.done()); - assert.strictEqual(token, 'abc123'); - }); + it('should allow passing scopes to get a client', async () => { + const scopes = ['http://examples.com/is/a/scope']; + const keyFilename = './test/fixtures/private.json'; + const auth = new GoogleAuth({scopes, keyFilename}); + const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.scopes, scopes); + }); - it('should get request headers', async () => { - const {auth, scopes} = mockGCE(); - scopes.push(createGetProjectIdNock()); - const headers = await auth.getRequestHeaders(); - scopes.forEach(s => s.done()); - assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); - }); + it('should allow passing a scope to get a client', async () => { + const scopes = 'http://examples.com/is/a/scope'; + const keyFilename = './test/fixtures/private.json'; + const auth = new GoogleAuth({scopes, keyFilename}); + const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.scopes, scopes); + }); - it('should authorize the request', async () => { - const {auth, scopes} = mockGCE(); - scopes.push(createGetProjectIdNock()); - const opts = await auth.authorizeRequest({url: 'http://example.com'}); - scopes.forEach(s => s.done()); - assert.deepStrictEqual(opts.headers, {Authorization: 'Bearer abc123'}); - }); + it('should allow passing a scope to get a Compute client', async () => { + const scopes = ['http://examples.com/is/a/scope']; + const nockScopes = [nockIsGCE(), createGetProjectIdNock()]; + const auth = new GoogleAuth({scopes}); + const client = (await auth.getClient()) as Compute; + assert.strictEqual(client.scopes, scopes); + nockScopes.forEach(x => x.done()); + }); - it('should get the current environment if GCE', async () => { - envDetect.clear(); - const {auth, scopes} = mockGCE(); - const env = await auth.getEnv(); - assert.strictEqual(env, envDetect.GCPEnv.COMPUTE_ENGINE); - }); + it('should get an access token', async () => { + const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); + const token = await auth.getAccessToken(); + scopes.forEach(s => s.done()); + assert.strictEqual(token, 'abc123'); + }); - it('should get the current environment if GKE', async () => { - envDetect.clear(); - const {auth, scopes} = mockGCE(); - const scope = nock(host) - .get(`${instancePath}/attributes/cluster-name`) - .reply(200, {}, HEADERS); - const env = await auth.getEnv(); - assert.strictEqual(env, envDetect.GCPEnv.KUBERNETES_ENGINE); - scope.done(); - }); + it('should get request headers', async () => { + const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); + const headers = await auth.getRequestHeaders(); + scopes.forEach(s => s.done()); + assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); + }); - it('should cache prior call to getEnv(), when GCE', async () => { - envDetect.clear(); - const {auth, scopes} = mockGCE(); - auth.getEnv(); - const env = await auth.getEnv(); - assert.strictEqual(env, envDetect.GCPEnv.COMPUTE_ENGINE); - }); + it('should authorize the request', async () => { + const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); + const opts = await auth.authorizeRequest({url: 'http://example.com'}); + scopes.forEach(s => s.done()); + assert.deepStrictEqual(opts.headers, {Authorization: 'Bearer abc123'}); + }); - it('should cache prior call to getEnv(), when GKE', async () => { - envDetect.clear(); - const {auth, scopes} = mockGCE(); - const scope = nock(host) - .get(`${instancePath}/attributes/cluster-name`) - .reply(200, {}, HEADERS); - auth.getEnv(); - const env = await auth.getEnv(); - assert.strictEqual(env, envDetect.GCPEnv.KUBERNETES_ENGINE); - scope.done(); - }); + it('should get the current environment if GCE', async () => { + envDetect.clear(); + const {auth, scopes} = mockGCE(); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.COMPUTE_ENGINE); + }); - it('should get the current environment if GCF 8 and below', async () => { - envDetect.clear(); - mockEnvVar('FUNCTION_NAME', 'DOGGY'); - const env = await auth.getEnv(); - assert.strictEqual(env, envDetect.GCPEnv.CLOUD_FUNCTIONS); - }); + it('should get the current environment if GKE', async () => { + envDetect.clear(); + const {auth, scopes} = mockGCE(); + const scope = nock(host) + .get(`${instancePath}/attributes/cluster-name`) + .reply(200, {}, HEADERS); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.KUBERNETES_ENGINE); + scope.done(); + }); - it('should get the current environment if GCF 10 and up', async () => { - envDetect.clear(); - mockEnvVar('FUNCTION_TARGET', 'KITTY'); - const env = await auth.getEnv(); - assert.strictEqual(env, envDetect.GCPEnv.CLOUD_FUNCTIONS); - }); + it('should cache prior call to getEnv(), when GCE', async () => { + envDetect.clear(); + const {auth, scopes} = mockGCE(); + auth.getEnv(); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.COMPUTE_ENGINE); + }); - it('should get the current environment if GAE', async () => { - envDetect.clear(); - mockEnvVar('GAE_SERVICE', 'KITTY'); - const env = await auth.getEnv(); - assert.strictEqual(env, envDetect.GCPEnv.APP_ENGINE); - }); + it('should cache prior call to getEnv(), when GKE', async () => { + envDetect.clear(); + const {auth, scopes} = mockGCE(); + const scope = nock(host) + .get(`${instancePath}/attributes/cluster-name`) + .reply(200, {}, HEADERS); + auth.getEnv(); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.KUBERNETES_ENGINE); + scope.done(); + }); - it('should make the request', async () => { - const url = 'http://example.com'; - const {auth, scopes} = mockGCE(); - scopes.push(createGetProjectIdNock()); - const data = {breakfast: 'coffee'}; - scopes.push( - nock(url) - .get('/') - .reply(200, data) - ); - const res = await auth.request({url}); - scopes.forEach(s => s.done()); - assert.deepStrictEqual(res.data, data); - }); + it('should get the current environment if GCF 8 and below', async () => { + envDetect.clear(); + mockEnvVar('FUNCTION_NAME', 'DOGGY'); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.CLOUD_FUNCTIONS); + }); - it('sign should use the private key for JWT clients', async () => { - const data = 'abc123'; - const auth = new GoogleAuth({ - credentials: { - client_email: 'google@auth.library', - private_key: privateKey, - }, - }); - const value = await auth.sign(data); - const sign = crypto.createSign('RSA-SHA256'); - sign.update(data); - const computed = sign.sign(privateKey, 'base64'); - assert.strictEqual(value, computed); - }); + it('should get the current environment if GCF 10 and up', async () => { + envDetect.clear(); + mockEnvVar('FUNCTION_TARGET', 'KITTY'); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.CLOUD_FUNCTIONS); + }); - it('sign should hit the IAM endpoint if no private_key is available', async () => { - const {auth, scopes} = mockGCE(); - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - const email = 'google@auth.library'; - const iamUri = `https://iam.googleapis.com`; - const iamPath = `/v1/projects/${STUB_PROJECT}/serviceAccounts/${email}:signBlob`; - const signature = 'erutangis'; - const data = 'abc123'; - scopes.push( - nock(iamUri) - .post(iamPath) - .reply(200, {signature}), - nock(host) - .get(svcAccountPath) - .reply(200, {default: {email, private_key: privateKey}}, HEADERS) - ); - const value = await auth.sign(data); - scopes.forEach(x => x.done()); - assert.strictEqual(value, signature); - }); + it('should get the current environment if GAE', async () => { + envDetect.clear(); + mockEnvVar('GAE_SERVICE', 'KITTY'); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.APP_ENGINE); + }); - // tslint:disable-next-line ban - it.skip('should warn the user if using default Cloud SDK credentials', done => { - exposeLinuxWellKnownFile = true; - createLinuxWellKnownStream = () => - fs.createReadStream('./test/fixtures/wellKnown.json'); - sandbox.stub(process, 'emitWarning').callsFake((message, warningOrType) => { - assert.strictEqual( - message, - messages.PROBLEMATIC_CREDENTIALS_WARNING.message + it('should make the request', async () => { + const url = 'http://example.com'; + const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); + const data = {breakfast: 'coffee'}; + scopes.push( + nock(url) + .get('/') + .reply(200, data) ); - const warningType = - typeof warningOrType === 'string' - ? warningOrType - : // @types/node doesn't recognize the emitWarning syntax which - // tslint:disable-next-line no-any - (warningOrType as any).type; - assert.strictEqual(warningType, messages.WarningTypes.WARNING); - done(); - }); - auth._tryGetApplicationCredentialsFromWellKnownFile(); - }); + const res = await auth.request({url}); + scopes.forEach(s => s.done()); + assert.deepStrictEqual(res.data, data); + }); - it('should warn the user if using the getDefaultProjectId method', done => { - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - sandbox.stub(process, 'emitWarning').callsFake((message, warningOrType) => { - assert.strictEqual( - message, - messages.DEFAULT_PROJECT_ID_DEPRECATED.message + it('sign should use the private key for JWT clients', async () => { + const data = 'abc123'; + const auth = new GoogleAuth({ + credentials: { + client_email: 'google@auth.library', + private_key: privateKey, + }, + }); + const value = await auth.sign(data); + const sign = crypto.createSign('RSA-SHA256'); + sign.update(data); + const computed = sign.sign(privateKey, 'base64'); + assert.strictEqual(value, computed); + }); + + it('sign should hit the IAM endpoint if no private_key is available', async () => { + const {auth, scopes} = mockGCE(); + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + const email = 'google@auth.library'; + const iamUri = `https://iam.googleapis.com`; + const iamPath = `/v1/projects/${STUB_PROJECT}/serviceAccounts/${email}:signBlob`; + const signature = 'erutangis'; + const data = 'abc123'; + scopes.push( + nock(iamUri) + .post(iamPath) + .reply(200, {signature}), + nock(host) + .get(svcAccountPath) + .reply(200, {default: {email, private_key: privateKey}}, HEADERS) ); - const warningType = - typeof warningOrType === 'string' - ? warningOrType - : // @types/node doesn't recognize the emitWarning syntax which - // tslint:disable-next-line no-any - (warningOrType as any).type; - assert.strictEqual(warningType, messages.WarningTypes.DEPRECATION); - done(); - }); - auth.getDefaultProjectId(); - }); + const value = await auth.sign(data); + scopes.forEach(x => x.done()); + assert.strictEqual(value, signature); + }); - it('should only emit warnings once', async () => { - // The warning was used above, so invoking it here should have no effect. - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - let count = 0; - sandbox.stub(process, 'emitWarning').callsFake(() => count++); - await auth.getDefaultProjectId(); - assert.strictEqual(count, 0); - }); + // tslint:disable-next-line ban + it.skip('should warn the user if using default Cloud SDK credentials', done => { + exposeLinuxWellKnownFile = true; + createLinuxWellKnownStream = () => + fs.createReadStream('./test/fixtures/wellKnown.json'); + sandbox + .stub(process, 'emitWarning') + .callsFake((message, warningOrType) => { + assert.strictEqual( + message, + messages.PROBLEMATIC_CREDENTIALS_WARNING.message + ); + const warningType = + typeof warningOrType === 'string' + ? warningOrType + : // @types/node doesn't recognize the emitWarning syntax which + // tslint:disable-next-line no-any + (warningOrType as any).type; + assert.strictEqual(warningType, messages.WarningTypes.WARNING); + done(); + }); + auth._tryGetApplicationCredentialsFromWellKnownFile(); + }); - it('should pass options to the JWT constructor via constructor', async () => { - const subject = 'science!'; - const auth = new GoogleAuth({ - keyFilename: './test/fixtures/private.json', - clientOptions: {subject}, + it('should warn the user if using the getDefaultProjectId method', done => { + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + sandbox + .stub(process, 'emitWarning') + .callsFake((message, warningOrType) => { + assert.strictEqual( + message, + messages.DEFAULT_PROJECT_ID_DEPRECATED.message + ); + const warningType = + typeof warningOrType === 'string' + ? warningOrType + : // @types/node doesn't recognize the emitWarning syntax which + // tslint:disable-next-line no-any + (warningOrType as any).type; + assert.strictEqual(warningType, messages.WarningTypes.DEPRECATION); + done(); + }); + auth.getDefaultProjectId(); }); - const client = (await auth.getClient()) as JWT; - assert.strictEqual(client.subject, subject); - }); - it('should throw if getProjectId cannot find a projectId', async () => { - // tslint:disable-next-line no-any - sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); - await assertRejects( - auth.getProjectId(), - /Unable to detect a Project Id in the current environment/ - ); - }); + it('should only emit warnings once', async () => { + // The warning was used above, so invoking it here should have no effect. + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + let count = 0; + sandbox.stub(process, 'emitWarning').callsFake(() => count++); + await auth.getDefaultProjectId(); + assert.strictEqual(count, 0); + }); - it('should throw if options are passed to getClient()', async () => { - const auth = new GoogleAuth(); - await assertRejects( - auth.getClient({hello: 'world'}), - /Passing options to getClient is forbidden in v5.0.0/ - ); - }); + it('should pass options to the JWT constructor via constructor', async () => { + const subject = 'science!'; + const auth = new GoogleAuth({ + keyFilename: './test/fixtures/private.json', + clientOptions: {subject}, + }); + const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.subject, subject); + }); - it('getRequestHeaders populates x-goog-user-project with quota_project if present', async () => { - const tokenReq = mockApplicationDefaultCredentials( - './test/fixtures/config-with-quota' - ); - const auth = new GoogleAuth(); - const headers = await auth.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); - tokenReq.done(); - }); + it('should throw if getProjectId cannot find a projectId', async () => { + // tslint:disable-next-line no-any + sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); + await assertRejects( + auth.getProjectId(), + /Unable to detect a Project Id in the current environment/ + ); + }); - it('getRequestHeaders does not populate x-goog-user-project if quota_project is not present', async () => { - const tokenReq = mockApplicationDefaultCredentials( - './test/fixtures/config-no-quota' - ); - const auth = new GoogleAuth(); - const headers = await auth.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], undefined); - tokenReq.done(); - }); + it('should throw if options are passed to getClient()', async () => { + const auth = new GoogleAuth(); + await assertRejects( + auth.getClient({hello: 'world'}), + /Passing options to getClient is forbidden in v5.0.0/ + ); + }); - it('getRequestHeaders populates x-goog-user-project when called on returned client', async () => { - const tokenReq = mockApplicationDefaultCredentials( - './test/fixtures/config-with-quota' - ); - const auth = new GoogleAuth(); - const client = await auth.getClient(); - assert(client instanceof UserRefreshClient); - const headers = await client.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); - tokenReq.done(); - }); + it('getRequestHeaders populates x-goog-user-project with quota_project if present', async () => { + const tokenReq = mockApplicationDefaultCredentials( + './test/fixtures/config-with-quota' + ); + const auth = new GoogleAuth(); + const headers = await auth.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + tokenReq.done(); + }); - it('populates x-goog-user-project when request is made', async () => { - const tokenReq = mockApplicationDefaultCredentials( - './test/fixtures/config-with-quota' - ); - const auth = new GoogleAuth(); - const client = await auth.getClient(); - assert(client instanceof UserRefreshClient); - const apiReq = nock(BASE_URL) - .post(ENDPOINT) - .reply(function(uri) { - assert.strictEqual( - this.req.headers['x-goog-user-project'][0], - 'my-quota-project' - ); - return [200, RESPONSE_BODY]; + it('getRequestHeaders does not populate x-goog-user-project if quota_project is not present', async () => { + const tokenReq = mockApplicationDefaultCredentials( + './test/fixtures/config-no-quota' + ); + const auth = new GoogleAuth(); + const headers = await auth.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], undefined); + tokenReq.done(); + }); + + it('getRequestHeaders populates x-goog-user-project when called on returned client', async () => { + const tokenReq = mockApplicationDefaultCredentials( + './test/fixtures/config-with-quota' + ); + const auth = new GoogleAuth(); + const client = await auth.getClient(); + assert(client instanceof UserRefreshClient); + const headers = await client.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + tokenReq.done(); + }); + + it('populates x-goog-user-project when request is made', async () => { + const tokenReq = mockApplicationDefaultCredentials( + './test/fixtures/config-with-quota' + ); + const auth = new GoogleAuth(); + const client = await auth.getClient(); + assert(client instanceof UserRefreshClient); + const apiReq = nock(BASE_URL) + .post(ENDPOINT) + .reply(function(uri) { + assert.strictEqual( + this.req.headers['x-goog-user-project'][0], + 'my-quota-project' + ); + return [200, RESPONSE_BODY]; + }); + const res = await client.request({ + url: BASE_URL + ENDPOINT, + method: 'POST', + data: {test: true}, }); - const res = await client.request({ - url: BASE_URL + ENDPOINT, - method: 'POST', - data: {test: true}, - }); - assert.strictEqual(RESPONSE_BODY, res.data); - tokenReq.done(); - apiReq.done(); - }); + assert.strictEqual(RESPONSE_BODY, res.data); + tokenReq.done(); + apiReq.done(); + }); - it('should return a Compute client for getIdTokenClient', async () => { - const nockScopes = [nockIsGCE(), createGetProjectIdNock()]; - const auth = new GoogleAuth(); - const client = await auth.getIdTokenClient('a-target-audience'); - assert(client instanceof IdTokenClient); - assert(client.idTokenProvider instanceof Compute); - }); + it('should return a Compute client for getIdTokenClient', async () => { + const nockScopes = [nockIsGCE(), createGetProjectIdNock()]; + const auth = new GoogleAuth(); + const client = await auth.getIdTokenClient('a-target-audience'); + assert(client instanceof IdTokenClient); + assert(client.idTokenProvider instanceof Compute); + }); - it('should return a JWT client for getIdTokenClient', async () => { - // Set up a mock to return path to a valid credentials file. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' - ); - - const auth = new GoogleAuth(); - const client = await auth.getIdTokenClient('a-target-audience'); - assert(client instanceof IdTokenClient); - assert(client.idTokenProvider instanceof JWT); - }); + it('should return a JWT client for getIdTokenClient', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); - it('should call getClient for getIdTokenClient', async () => { - // Set up a mock to return path to a valid credentials file. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' - ); - - const spy = sinon.spy(auth, 'getClient'); - const client = await auth.getIdTokenClient('a-target-audience'); - assert(client instanceof IdTokenClient); - assert(spy.calledOnce); - }); + const auth = new GoogleAuth(); + const client = await auth.getIdTokenClient('a-target-audience'); + assert(client instanceof IdTokenClient); + assert(client.idTokenProvider instanceof JWT); + }); - it('should fail when using UserRefreshClient', async () => { - // Set up a mock to return path to a valid credentials file. - mockEnvVar( - 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/refresh.json' - ); - mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id'); + it('should call getClient for getIdTokenClient', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); - try { + const spy = sinon.spy(auth, 'getClient'); const client = await auth.getIdTokenClient('a-target-audience'); - } catch (e) { - assert(e.message.startsWith('Cannot fetch ID token in this environment')); - return; + assert(client instanceof IdTokenClient); + assert(spy.calledOnce); + }); + + it('should fail when using UserRefreshClient', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/refresh.json' + ); + mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id'); + + try { + const client = await auth.getIdTokenClient('a-target-audience'); + } catch (e) { + assert( + e.message.startsWith('Cannot fetch ID token in this environment') + ); + return; + } + assert.fail('failed to throw'); + }); + + function mockApplicationDefaultCredentials(path: string) { + // Fake a home directory in our fixtures path. + mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); + mockEnvVar('HOME', path); + mockEnvVar('APPDATA', `${path}/.config`); + // The first time auth.getClient() is called /token endpoint is used to + // fetch a JWT. + return nock('https://oauth2.googleapis.com') + .post('/token') + .reply(200, {}); } - assert.fail('failed to throw'); }); - - function mockApplicationDefaultCredentials(path: string) { - // Fake a home directory in our fixtures path. - mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); - mockEnvVar('HOME', path); - mockEnvVar('APPDATA', `${path}/.config`); - // The first time auth.getClient() is called /token endpoint is used to - // fetch a JWT. - return nock('https://oauth2.googleapis.com') - .post('/token') - .reply(200, {}); - } }); diff --git a/test/test.iam.ts b/test/test.iam.ts index f86f38d0..e382916a 100644 --- a/test/test.iam.ts +++ b/test/test.iam.ts @@ -13,44 +13,45 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {describe, it, beforeEach, afterEach} from 'mocha'; import * as sinon from 'sinon'; - import {IAMAuth} from '../src'; import * as messages from '../src/messages'; -const testSelector = 'a-test-selector'; -const testToken = 'a-test-token'; +describe('iam', () => { + const testSelector = 'a-test-selector'; + const testToken = 'a-test-token'; -let sandbox: sinon.SinonSandbox; -let client: IAMAuth; -beforeEach(() => { - sandbox = sinon.createSandbox(); - client = new IAMAuth(testSelector, testToken); -}); -afterEach(() => { - sandbox.restore(); -}); + let sandbox: sinon.SinonSandbox; + let client: IAMAuth; + beforeEach(() => { + sandbox = sinon.createSandbox(); + client = new IAMAuth(testSelector, testToken); + }); + afterEach(() => { + sandbox.restore(); + }); -it('passes the token and selector to the callback ', async () => { - const creds = client.getRequestHeaders(); - assert.notStrictEqual(creds, null, 'metadata should be present'); - assert.strictEqual(creds!['x-goog-iam-authority-selector'], testSelector); - assert.strictEqual(creds!['x-goog-iam-authorization-token'], testToken); -}); + it('passes the token and selector to the callback ', async () => { + const creds = client.getRequestHeaders(); + assert.notStrictEqual(creds, null, 'metadata should be present'); + assert.strictEqual(creds!['x-goog-iam-authority-selector'], testSelector); + assert.strictEqual(creds!['x-goog-iam-authorization-token'], testToken); + }); -it('should warn about deprecation of getRequestMetadata', done => { - const stub = sandbox.stub(messages, 'warn'); - // tslint:disable-next-line deprecation - client.getRequestMetadata(null, () => { - assert.strictEqual(stub.calledOnce, true); - done(); + it('should warn about deprecation of getRequestMetadata', done => { + const stub = sandbox.stub(messages, 'warn'); + // tslint:disable-next-line deprecation + client.getRequestMetadata(null, () => { + assert.strictEqual(stub.calledOnce, true); + done(); + }); }); -}); -it('should emit warning for createScopedRequired', () => { - const stub = sandbox.stub(process, 'emitWarning'); - // tslint:disable-next-line deprecation - client.createScopedRequired(); - assert(stub.called); + it('should emit warning for createScopedRequired', () => { + const stub = sandbox.stub(process, 'emitWarning'); + // tslint:disable-next-line deprecation + client.createScopedRequired(); + assert(stub.called); + }); }); diff --git a/test/test.idtokenclient.ts b/test/test.idtokenclient.ts index bfe88a8f..8dabdc80 100644 --- a/test/test.idtokenclient.ts +++ b/test/test.idtokenclient.ts @@ -11,79 +11,82 @@ // 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. + import * as assert from 'assert'; -import {it} from 'mocha'; +import {describe, it, afterEach} from 'mocha'; import * as fs from 'fs'; import * as nock from 'nock'; import {IdTokenClient, JWT} from '../src'; import {CredentialRequest} from '../src/auth/credentials'; -const PEM_PATH = './test/fixtures/private.pem'; -const PEM_CONTENTS = fs.readFileSync(PEM_PATH, 'utf8'); -nock.disableNetConnect(); - -function createGTokenMock(body: CredentialRequest) { - return nock('https://www.googleapis.com') - .post('/oauth2/v4/token') - .reply(200, body); -} +describe('idtokenclient', () => { + const PEM_PATH = './test/fixtures/private.pem'; + const PEM_CONTENTS = fs.readFileSync(PEM_PATH, 'utf8'); + nock.disableNetConnect(); -afterEach(() => { - nock.cleanAll(); -}); + function createGTokenMock(body: CredentialRequest) { + return nock('https://www.googleapis.com') + .post('/oauth2/v4/token') + .reply(200, body); + } -it('should determine expiry_date from JWT', async () => { - const idToken = 'header.eyJleHAiOiAxNTc4NzAyOTU2fQo.signature'; - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: PEM_CONTENTS, - subject: 'ignored@subjectaccount.com', + afterEach(() => { + nock.cleanAll(); }); - const scope = createGTokenMock({id_token: idToken}); - const targetAudience = 'a-target-audience'; - const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); - await client.getRequestHeaders(); - scope.done(); - assert.strictEqual(client.credentials.expiry_date, 1578702956000); -}); + it('should determine expiry_date from JWT', async () => { + const idToken = 'header.eyJleHAiOiAxNTc4NzAyOTU2fQo.signature'; + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); -it('should refresh ID token if expired', async () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: PEM_CONTENTS, - subject: 'ignored@subjectaccount.com', + const scope = createGTokenMock({id_token: idToken}); + const targetAudience = 'a-target-audience'; + const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); + await client.getRequestHeaders(); + scope.done(); + assert.strictEqual(client.credentials.expiry_date, 1578702956000); }); - const scope = createGTokenMock({id_token: 'abc123'}); - const targetAudience = 'a-target-audience'; - const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); - client.credentials = { - id_token: 'an-identity-token', - expiry_date: new Date().getTime() - 1000, - }; - const headers = await client.getRequestHeaders(); - scope.done(); - assert.strictEqual(client.credentials.id_token, 'abc123'); - assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); -}); + it('should refresh ID token if expired', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); -it('should refresh ID token if expiry_date not set', async () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: PEM_CONTENTS, - subject: 'ignored@subjectaccount.com', + const scope = createGTokenMock({id_token: 'abc123'}); + const targetAudience = 'a-target-audience'; + const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); + client.credentials = { + id_token: 'an-identity-token', + expiry_date: new Date().getTime() - 1000, + }; + const headers = await client.getRequestHeaders(); + scope.done(); + assert.strictEqual(client.credentials.id_token, 'abc123'); + assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); }); - const scope = createGTokenMock({id_token: 'abc123'}); - const targetAudience = 'a-target-audience'; - const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); - client.credentials = { - id_token: 'an-identity-token', - }; - const headers = await client.getRequestHeaders(); - scope.done(); - assert.strictEqual(client.credentials.id_token, 'abc123'); - assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); + it('should refresh ID token if expiry_date not set', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); + + const scope = createGTokenMock({id_token: 'abc123'}); + const targetAudience = 'a-target-audience'; + const client = new IdTokenClient({idTokenProvider: jwt, targetAudience}); + client.credentials = { + id_token: 'an-identity-token', + }; + const headers = await client.getRequestHeaders(); + scope.done(); + assert.strictEqual(client.credentials.id_token, 'abc123'); + assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); + }); }); diff --git a/test/test.index.ts b/test/test.index.ts index 00d197ad..935f21c1 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -15,24 +15,26 @@ import * as assert from 'assert'; import {describe, it} from 'mocha'; import * as gal from '../src'; -it('should publicly export GoogleAuth', () => { - const cjs = require('../src/'); - assert.strictEqual(cjs.GoogleAuth, gal.GoogleAuth); -}); +describe('index', () => { + it('should publicly export GoogleAuth', () => { + const cjs = require('../src/'); + assert.strictEqual(cjs.GoogleAuth, gal.GoogleAuth); + }); -it('should publicly export DefaultTransporter', () => { - const cjs = require('../src'); - assert.strictEqual(cjs.DefaultTransporter, gal.DefaultTransporter); -}); + it('should publicly export DefaultTransporter', () => { + const cjs = require('../src'); + assert.strictEqual(cjs.DefaultTransporter, gal.DefaultTransporter); + }); -it('should export all the things', () => { - assert(gal.CodeChallengeMethod); - assert(gal.Compute); - assert(gal.DefaultTransporter); - assert(gal.IAMAuth); - assert(gal.JWT); - assert(gal.JWTAccess); - assert(gal.OAuth2Client); - assert(gal.UserRefreshClient); - assert(gal.GoogleAuth); + it('should export all the things', () => { + assert(gal.CodeChallengeMethod); + assert(gal.Compute); + assert(gal.DefaultTransporter); + assert(gal.IAMAuth); + assert(gal.JWT); + assert(gal.JWTAccess); + assert(gal.OAuth2Client); + assert(gal.UserRefreshClient); + assert(gal.GoogleAuth); + }); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 12d00916..339e1406 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {describe, it, beforeEach, afterEach} from 'mocha'; import * as fs from 'fs'; import * as jws from 'jws'; import * as nock from 'nock'; @@ -22,841 +22,846 @@ import * as sinon from 'sinon'; import {GoogleAuth, JWT} from '../src'; import {CredentialRequest, JWTInput} from '../src/auth/credentials'; -const keypair = require('keypair'); -const PEM_PATH = './test/fixtures/private.pem'; -const PEM_CONTENTS = fs.readFileSync(PEM_PATH, 'utf8'); -const P12_PATH = './test/fixtures/key.p12'; - -nock.disableNetConnect(); - -// Creates a standard JSON credentials object for testing. -function createJSON() { - return { - private_key_id: 'key123', - private_key: 'privatekey', - client_email: 'hello@youarecool.com', - client_id: 'client123', - type: 'service_account', - }; -} - -function createRefreshJSON() { - return { - client_secret: 'privatekey', - client_id: 'client123', - refresh_token: 'refreshtoken', - type: 'authorized_user', - }; -} - -function createGTokenMock(body: CredentialRequest) { - return nock('https://www.googleapis.com') - .post('/oauth2/v4/token') - .reply(200, body); -} - -// set up the test json and the jwt instance being tested. -let jwt: JWT; -let json: JWTInput; -let sandbox: sinon.SinonSandbox; -beforeEach(() => { - json = createJSON(); - jwt = new JWT(); - sandbox = sinon.createSandbox(); -}); - -afterEach(() => { - nock.cleanAll(); - sandbox.restore(); -}); +describe('jwt', () => { + const keypair = require('keypair'); + const PEM_PATH = './test/fixtures/private.pem'; + const PEM_CONTENTS = fs.readFileSync(PEM_PATH, 'utf8'); + const P12_PATH = './test/fixtures/key.p12'; + + nock.disableNetConnect(); + + // Creates a standard JSON credentials object for testing. + function createJSON() { + return { + private_key_id: 'key123', + private_key: 'privatekey', + client_email: 'hello@youarecool.com', + client_id: 'client123', + type: 'service_account', + }; + } -it('should emit warning for createScopedRequired', () => { - let called = false; - sandbox.stub(process, 'emitWarning').callsFake(() => (called = true)); - // tslint:disable-next-line deprecation - jwt.createScopedRequired(); - assert.strictEqual(called, true); -}); + function createRefreshJSON() { + return { + client_secret: 'privatekey', + client_id: 'client123', + refresh_token: 'refreshtoken', + type: 'authorized_user', + }; + } -it('should create a dummy refresh token string', () => { - // It is important that the compute client is created with a refresh token - // value filled in, or else the rest of the logic will not work. - const jwt = new JWT(); - assert.strictEqual('jwt-placeholder', jwt.credentials.refresh_token); -}); + function createGTokenMock(body: CredentialRequest) { + return nock('https://www.googleapis.com') + .post('/oauth2/v4/token') + .reply(200, body); + } -it('should get an initial access token', done => { - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); - const scope = createGTokenMock({access_token: 'initial-access-token'}); - jwt.authorize((err, creds) => { - scope.done(); - assert.strictEqual(err, null); - assert.notStrictEqual(creds, null); - assert.strictEqual('foo@serviceaccount.com', jwt.gtoken!.iss); - assert.strictEqual(PEM_PATH, jwt.gtoken!.keyFile); - assert.strictEqual( - ['http://bar', 'http://foo'].join(' '), - jwt.gtoken!.scope - ); - assert.strictEqual('bar@subjectaccount.com', jwt.gtoken!.sub); - assert.strictEqual('initial-access-token', jwt.credentials.access_token); - assert.strictEqual(creds!.access_token, jwt.credentials.access_token); - assert.strictEqual(creds!.refresh_token, jwt.credentials.refresh_token); - assert.strictEqual(creds!.token_type, jwt.credentials.token_type); - assert.strictEqual('jwt-placeholder', jwt.credentials.refresh_token); - assert.strictEqual(PEM_CONTENTS, jwt.key); - assert.strictEqual('foo@serviceaccount.com', jwt.email); - done(); + // set up the test json and the jwt instance being tested. + let jwt: JWT; + let json: JWTInput; + let sandbox: sinon.SinonSandbox; + beforeEach(() => { + json = createJSON(); + jwt = new JWT(); + sandbox = sinon.createSandbox(); }); -}); -it('should accept scope as string', done => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: 'http://foo', - subject: 'bar@subjectaccount.com', + afterEach(() => { + nock.cleanAll(); + sandbox.restore(); }); - const scope = createGTokenMock({access_token: 'initial-access-token'}); - jwt.authorize((err, creds) => { - scope.done(); - assert.strictEqual('http://foo', jwt.gtoken!.scope); - done(); + it('should emit warning for createScopedRequired', () => { + let called = false; + sandbox.stub(process, 'emitWarning').callsFake(() => (called = true)); + // tslint:disable-next-line deprecation + jwt.createScopedRequired(); + assert.strictEqual(called, true); }); -}); -it('can get obtain new access token when scopes are set', done => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', + it('should create a dummy refresh token string', () => { + // It is important that the compute client is created with a refresh token + // value filled in, or else the rest of the logic will not work. + const jwt = new JWT(); + assert.strictEqual('jwt-placeholder', jwt.credentials.refresh_token); }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - const scope = createGTokenMock({access_token: 'initial-access-token'}); - jwt.getAccessToken((err, got) => { - scope.done(); - assert.strictEqual(null, err, 'no error was expected: got\n' + err); - assert.strictEqual( - 'initial-access-token', - got, - 'the access token was wrong: ' + got + it('should get an initial access token', done => { + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' ); - done(); - }); -}); - -it('should emit an event for tokens', done => { - const accessToken = 'initial-access-token'; - const scope = createGTokenMock({access_token: accessToken}); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - jwt - .on('tokens', tokens => { - assert.strictEqual(tokens.access_token, accessToken); + const scope = createGTokenMock({access_token: 'initial-access-token'}); + jwt.authorize((err, creds) => { scope.done(); + assert.strictEqual(err, null); + assert.notStrictEqual(creds, null); + assert.strictEqual('foo@serviceaccount.com', jwt.gtoken!.iss); + assert.strictEqual(PEM_PATH, jwt.gtoken!.keyFile); + assert.strictEqual( + ['http://bar', 'http://foo'].join(' '), + jwt.gtoken!.scope + ); + assert.strictEqual('bar@subjectaccount.com', jwt.gtoken!.sub); + assert.strictEqual('initial-access-token', jwt.credentials.access_token); + assert.strictEqual(creds!.access_token, jwt.credentials.access_token); + assert.strictEqual(creds!.refresh_token, jwt.credentials.refresh_token); + assert.strictEqual(creds!.token_type, jwt.credentials.token_type); + assert.strictEqual('jwt-placeholder', jwt.credentials.refresh_token); + assert.strictEqual(PEM_CONTENTS, jwt.key); + assert.strictEqual('foo@serviceaccount.com', jwt.email); done(); - }) - .getAccessToken(); -}); - -it('can obtain new access token when scopes are set', async () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - - const wantedToken = 'abc123'; - const want = `Bearer ${wantedToken}`; - const scope = createGTokenMock({access_token: wantedToken}); - const headers = await jwt.getRequestHeaders(); - scope.done(); - assert.strictEqual( - want, - headers.Authorization, - `the authorization header was wrong: ${headers.Authorization}` - ); -}); - -it('gets a jwt header access token', async () => { - const keys = keypair(1024 /* bitsize of private key */); - const email = 'foo@serviceaccount.com'; - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: keys.private, - subject: 'ignored@subjectaccount.com', - }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - - const testUri = 'http:/example.com/my_test_service'; - const got = await jwt.getRequestHeaders(testUri); - assert.notStrictEqual(null, got, 'the creds should be present'); - const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); - assert.deepStrictEqual({alg: 'RS256', typ: 'JWT'}, decoded.header); - const payload = decoded.payload; - assert.strictEqual(email, payload.iss); - assert.strictEqual(email, payload.sub); - assert.strictEqual(testUri, payload.aud); -}); - -it('gets a jwt header access token with key id', async () => { - const keys = keypair(1024 /* bitsize of private key */); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: keys.private, - keyId: '101', - subject: 'ignored@subjectaccount.com', - }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - - const testUri = 'http:/example.com/my_test_service'; - const got = await jwt.getRequestHeaders(testUri); - assert.notStrictEqual(null, got, 'the creds should be present'); - const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); - assert.deepStrictEqual( - {alg: 'RS256', typ: 'JWT', kid: '101'}, - decoded.header - ); -}); - -it('should accept additionalClaims', async () => { - const keys = keypair(1024 /* bitsize of private key */); - const someClaim = 'cat-on-my-desk'; - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: keys.private, - subject: 'ignored@subjectaccount.com', - additionalClaims: {someClaim}, - }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - - const testUri = 'http:/example.com/my_test_service'; - const got = await jwt.getRequestHeaders(testUri); - assert.notStrictEqual(null, got, 'the creds should be present'); - const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); - const payload = decoded.payload; - assert.strictEqual(testUri, payload.aud); - assert.strictEqual(someClaim, payload.someClaim); -}); - -it('should accept additionalClaims that include a target_audience', async () => { - const keys = keypair(1024 /* bitsize of private key */); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: keys.private, - subject: 'ignored@subjectaccount.com', - additionalClaims: {target_audience: 'applause'}, - }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - - const testUri = 'http:/example.com/my_test_service'; - const scope = createGTokenMock({id_token: 'abc123'}); - const got = await jwt.getRequestHeaders(testUri); - scope.done(); - assert.notStrictEqual(null, got, 'the creds should be present'); - const decoded = got.Authorization.replace('Bearer ', ''); - assert.strictEqual(decoded, 'abc123'); -}); - -it('should refresh token if missing access token', done => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', + }); }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - const scope = createGTokenMock({access_token: 'abc123'}); - jwt.request({url: 'http://bar'}, () => { - scope.done(); - assert.strictEqual('abc123', jwt.credentials.access_token); - done(); - }); -}); - -it('should unify the promise when refreshing the token', async () => { - // Mock a single call to the token server, and 3 calls to the example - // endpoint. This makes sure that refreshToken is called only once. - const scopes = [ - createGTokenMock({access_token: 'abc123'}), - nock('http://example.com') - .get('/') - .thrice() - .reply(200), - ]; - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - await Promise.all([ - jwt.request({url: 'http://example.com'}), - jwt.request({url: 'http://example.com'}), - jwt.request({url: 'http://example.com'}), - ]); - scopes.forEach(s => s.done()); - assert.strictEqual('abc123', jwt.credentials.access_token); -}); + it('should accept scope as string', done => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: 'http://foo', + subject: 'bar@subjectaccount.com', + }); -it('should clear the cached refresh token promise after completion', async () => { - // Mock 2 calls to the token server and 2 calls to the example endpoint. - // This makes sure that the token endpoint is invoked twice, preventing - // the promise from getting cached for too long. - const scopes = [ - createGTokenMock({access_token: 'abc123'}), - createGTokenMock({access_token: 'abc123'}), - nock('http://example.com') - .get('/') - .twice() - .reply(200), - ]; - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - jwt.credentials = {refresh_token: 'refresh-token-placeholder'}; - await jwt.request({url: 'http://example.com'}); - jwt.credentials.access_token = null; - await jwt.request({url: 'http://example.com'}); - scopes.forEach(s => s.done()); - assert.strictEqual('abc123', jwt.credentials.access_token); -}); - -it('should refresh token if expired', done => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', + const scope = createGTokenMock({access_token: 'initial-access-token'}); + jwt.authorize((err, creds) => { + scope.done(); + assert.strictEqual('http://foo', jwt.gtoken!.scope); + done(); + }); }); - jwt.credentials = { - access_token: 'woot', - refresh_token: 'jwt-placeholder', - expiry_date: new Date().getTime() - 1000, - }; + it('can get obtain new access token when scopes are set', done => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); - const scope = createGTokenMock({access_token: 'abc123'}); - jwt.request({url: 'http://bar'}, () => { + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + const scope = createGTokenMock({access_token: 'initial-access-token'}); + jwt.getAccessToken((err, got) => { + scope.done(); + assert.strictEqual(null, err, 'no error was expected: got\n' + err); + assert.strictEqual( + 'initial-access-token', + got, + 'the access token was wrong: ' + got + ); + done(); + }); + }); + + it('should emit an event for tokens', done => { + const accessToken = 'initial-access-token'; + const scope = createGTokenMock({access_token: accessToken}); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + jwt + .on('tokens', tokens => { + assert.strictEqual(tokens.access_token, accessToken); + scope.done(); + done(); + }) + .getAccessToken(); + }); + + it('can obtain new access token when scopes are set', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + + const wantedToken = 'abc123'; + const want = `Bearer ${wantedToken}`; + const scope = createGTokenMock({access_token: wantedToken}); + const headers = await jwt.getRequestHeaders(); scope.done(); - assert.strictEqual('abc123', jwt.credentials.access_token); - done(); + assert.strictEqual( + want, + headers.Authorization, + `the authorization header was wrong: ${headers.Authorization}` + ); }); -}); -it('should refresh if access token will expired soon and time to refresh before expiration is set', done => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - eagerRefreshThresholdMillis: 1000, + it('gets a jwt header access token', async () => { + const keys = keypair(1024 /* bitsize of private key */); + const email = 'foo@serviceaccount.com'; + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: keys.private, + subject: 'ignored@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + + const testUri = 'http:/example.com/my_test_service'; + const got = await jwt.getRequestHeaders(testUri); + assert.notStrictEqual(null, got, 'the creds should be present'); + const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); + assert.deepStrictEqual({alg: 'RS256', typ: 'JWT'}, decoded.header); + const payload = decoded.payload; + assert.strictEqual(email, payload.iss); + assert.strictEqual(email, payload.sub); + assert.strictEqual(testUri, payload.aud); + }); + + it('gets a jwt header access token with key id', async () => { + const keys = keypair(1024 /* bitsize of private key */); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: keys.private, + keyId: '101', + subject: 'ignored@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + + const testUri = 'http:/example.com/my_test_service'; + const got = await jwt.getRequestHeaders(testUri); + assert.notStrictEqual(null, got, 'the creds should be present'); + const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); + assert.deepStrictEqual( + {alg: 'RS256', typ: 'JWT', kid: '101'}, + decoded.header + ); }); - jwt.credentials = { - access_token: 'woot', - refresh_token: 'jwt-placeholder', - expiry_date: new Date().getTime() + 800, - }; - - const scope = createGTokenMock({access_token: 'abc123'}); - jwt.request({url: 'http://bar'}, () => { + it('should accept additionalClaims', async () => { + const keys = keypair(1024 /* bitsize of private key */); + const someClaim = 'cat-on-my-desk'; + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: keys.private, + subject: 'ignored@subjectaccount.com', + additionalClaims: {someClaim}, + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + + const testUri = 'http:/example.com/my_test_service'; + const got = await jwt.getRequestHeaders(testUri); + assert.notStrictEqual(null, got, 'the creds should be present'); + const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); + const payload = decoded.payload; + assert.strictEqual(testUri, payload.aud); + assert.strictEqual(someClaim, payload.someClaim); + }); + + it('should accept additionalClaims that include a target_audience', async () => { + const keys = keypair(1024 /* bitsize of private key */); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: keys.private, + subject: 'ignored@subjectaccount.com', + additionalClaims: {target_audience: 'applause'}, + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + + const testUri = 'http:/example.com/my_test_service'; + const scope = createGTokenMock({id_token: 'abc123'}); + const got = await jwt.getRequestHeaders(testUri); scope.done(); + assert.notStrictEqual(null, got, 'the creds should be present'); + const decoded = got.Authorization.replace('Bearer ', ''); + assert.strictEqual(decoded, 'abc123'); + }); + + it('should refresh token if missing access token', done => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + const scope = createGTokenMock({access_token: 'abc123'}); + + jwt.request({url: 'http://bar'}, () => { + scope.done(); + assert.strictEqual('abc123', jwt.credentials.access_token); + done(); + }); + }); + + it('should unify the promise when refreshing the token', async () => { + // Mock a single call to the token server, and 3 calls to the example + // endpoint. This makes sure that refreshToken is called only once. + const scopes = [ + createGTokenMock({access_token: 'abc123'}), + nock('http://example.com') + .get('/') + .thrice() + .reply(200), + ]; + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + await Promise.all([ + jwt.request({url: 'http://example.com'}), + jwt.request({url: 'http://example.com'}), + jwt.request({url: 'http://example.com'}), + ]); + scopes.forEach(s => s.done()); assert.strictEqual('abc123', jwt.credentials.access_token); - done(); - }); -}); - -it('should not refresh if access token will not expire soon and time to refresh before expiration is set', done => { - const scope = createGTokenMock({access_token: 'abc123', expires_in: 10000}); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - eagerRefreshThresholdMillis: 1000, }); - jwt.credentials = { - access_token: 'initial-access-token', - refresh_token: 'jwt-placeholder', - expiry_date: new Date().getTime() + 5000, - }; - - jwt.request({url: 'http://bar'}, () => { - assert.strictEqual('initial-access-token', jwt.credentials.access_token); - assert.strictEqual(false, scope.isDone()); - done(); - }); -}); - -it('should refresh token if the server returns 403', done => { - nock('http://example.com') - .get('/access') - .twice() - .reply(403); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://example.com'], - subject: 'bar@subjectaccount.com', - }); - - jwt.credentials = { - access_token: 'woot', - refresh_token: 'jwt-placeholder', - expiry_date: new Date().getTime() + 5000, - }; - - const scope = createGTokenMock({access_token: 'abc123'}); - - jwt.request({url: 'http://example.com/access'}, () => { - scope.done(); + it('should clear the cached refresh token promise after completion', async () => { + // Mock 2 calls to the token server and 2 calls to the example endpoint. + // This makes sure that the token endpoint is invoked twice, preventing + // the promise from getting cached for too long. + const scopes = [ + createGTokenMock({access_token: 'abc123'}), + createGTokenMock({access_token: 'abc123'}), + nock('http://example.com') + .get('/') + .twice() + .reply(200), + ]; + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'refresh-token-placeholder'}; + await jwt.request({url: 'http://example.com'}); + jwt.credentials.access_token = null; + await jwt.request({url: 'http://example.com'}); + scopes.forEach(s => s.done()); assert.strictEqual('abc123', jwt.credentials.access_token); - done(); }); -}); -it('should not refresh if not expired', done => { - const scope = createGTokenMock({access_token: 'abc123', expires_in: 10000}); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); + it('should refresh token if expired', done => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); - jwt.credentials = { - access_token: 'initial-access-token', - refresh_token: 'jwt-placeholder', - expiry_date: new Date().getTime() + 5000, - }; + jwt.credentials = { + access_token: 'woot', + refresh_token: 'jwt-placeholder', + expiry_date: new Date().getTime() - 1000, + }; - jwt.request({url: 'http://bar'}, () => { - assert.strictEqual('initial-access-token', jwt.credentials.access_token); - assert.strictEqual(false, scope.isDone()); - done(); + const scope = createGTokenMock({access_token: 'abc123'}); + jwt.request({url: 'http://bar'}, () => { + scope.done(); + assert.strictEqual('abc123', jwt.credentials.access_token); + done(); + }); + }); + + it('should refresh if access token will expired soon and time to refresh before expiration is set', done => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + eagerRefreshThresholdMillis: 1000, + }); + + jwt.credentials = { + access_token: 'woot', + refresh_token: 'jwt-placeholder', + expiry_date: new Date().getTime() + 800, + }; + + const scope = createGTokenMock({access_token: 'abc123'}); + jwt.request({url: 'http://bar'}, () => { + scope.done(); + assert.strictEqual('abc123', jwt.credentials.access_token); + done(); + }); + }); + + it('should not refresh if access token will not expire soon and time to refresh before expiration is set', done => { + const scope = createGTokenMock({access_token: 'abc123', expires_in: 10000}); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + eagerRefreshThresholdMillis: 1000, + }); + + jwt.credentials = { + access_token: 'initial-access-token', + refresh_token: 'jwt-placeholder', + expiry_date: new Date().getTime() + 5000, + }; + + jwt.request({url: 'http://bar'}, () => { + assert.strictEqual('initial-access-token', jwt.credentials.access_token); + assert.strictEqual(false, scope.isDone()); + done(); + }); }); -}); -it('should assume access token is not expired', done => { - const scope = createGTokenMock({access_token: 'abc123', expires_in: 10000}); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', + it('should refresh token if the server returns 403', done => { + nock('http://example.com') + .get('/access') + .twice() + .reply(403); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://example.com'], + subject: 'bar@subjectaccount.com', + }); + + jwt.credentials = { + access_token: 'woot', + refresh_token: 'jwt-placeholder', + expiry_date: new Date().getTime() + 5000, + }; + + const scope = createGTokenMock({access_token: 'abc123'}); + + jwt.request({url: 'http://example.com/access'}, () => { + scope.done(); + assert.strictEqual('abc123', jwt.credentials.access_token); + done(); + }); + }); + + it('should not refresh if not expired', done => { + const scope = createGTokenMock({access_token: 'abc123', expires_in: 10000}); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + + jwt.credentials = { + access_token: 'initial-access-token', + refresh_token: 'jwt-placeholder', + expiry_date: new Date().getTime() + 5000, + }; + + jwt.request({url: 'http://bar'}, () => { + assert.strictEqual('initial-access-token', jwt.credentials.access_token); + assert.strictEqual(false, scope.isDone()); + done(); + }); + }); + + it('should assume access token is not expired', done => { + const scope = createGTokenMock({access_token: 'abc123', expires_in: 10000}); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + + jwt.credentials = { + access_token: 'initial-access-token', + refresh_token: 'jwt-placeholder', + }; + + jwt.request({url: 'http://bar'}, () => { + assert.strictEqual('initial-access-token', jwt.credentials.access_token); + assert.strictEqual(false, scope.isDone()); + done(); + }); }); - jwt.credentials = { - access_token: 'initial-access-token', - refresh_token: 'jwt-placeholder', - }; + it('should return expiry_date in milliseconds', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); - jwt.request({url: 'http://bar'}, () => { - assert.strictEqual('initial-access-token', jwt.credentials.access_token); - assert.strictEqual(false, scope.isDone()); - done(); - }); -}); - -it('should return expiry_date in milliseconds', async () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - - const scope = createGTokenMock({access_token: 'token', expires_in: 100}); - jwt.credentials.access_token = null; - await jwt.getRequestHeaders(); - scope.done(); - const dateInMillis = new Date().getTime(); - assert.strictEqual( - dateInMillis.toString().length, - jwt.credentials.expiry_date!.toString().length - ); -}); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; -it('createScoped should clone stuff', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - keyId: '101', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', + const scope = createGTokenMock({access_token: 'token', expires_in: 100}); + jwt.credentials.access_token = null; + await jwt.getRequestHeaders(); + scope.done(); + const dateInMillis = new Date().getTime(); + assert.strictEqual( + dateInMillis.toString().length, + jwt.credentials.expiry_date!.toString().length + ); }); - const clone = jwt.createScoped('x'); - - assert.strictEqual(jwt.email, clone.email); - assert.strictEqual(jwt.keyFile, clone.keyFile); - assert.strictEqual(jwt.key, clone.key); - assert.strictEqual(jwt.keyId, clone.keyId); - assert.strictEqual(jwt.subject, clone.subject); -}); - -it('createScoped should handle string scope', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - const clone = jwt.createScoped('newscope'); - assert.strictEqual('newscope', clone.scopes); -}); - -it('createScoped should handle array scope', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - const clone = jwt.createScoped(['gorilla', 'chimpanzee', 'orangutan']); - assert.strictEqual(3, clone.scopes!.length); - assert.strictEqual('gorilla', clone.scopes![0]); - assert.strictEqual('chimpanzee', clone.scopes![1]); - assert.strictEqual('orangutan', clone.scopes![2]); -}); - -it('createScoped should handle null scope', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', + it('createScoped should clone stuff', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + keyId: '101', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + + const clone = jwt.createScoped('x'); + + assert.strictEqual(jwt.email, clone.email); + assert.strictEqual(jwt.keyFile, clone.keyFile); + assert.strictEqual(jwt.key, clone.key); + assert.strictEqual(jwt.keyId, clone.keyId); + assert.strictEqual(jwt.subject, clone.subject); + }); + + it('createScoped should handle string scope', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + const clone = jwt.createScoped('newscope'); + assert.strictEqual('newscope', clone.scopes); + }); + + it('createScoped should handle array scope', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + const clone = jwt.createScoped(['gorilla', 'chimpanzee', 'orangutan']); + assert.strictEqual(3, clone.scopes!.length); + assert.strictEqual('gorilla', clone.scopes![0]); + assert.strictEqual('chimpanzee', clone.scopes![1]); + assert.strictEqual('orangutan', clone.scopes![2]); + }); + + it('createScoped should handle null scope', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + const clone = jwt.createScoped(); + assert.strictEqual(undefined, clone.scopes); + }); + + it('createScoped should set scope when scope was null', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + subject: 'bar@subjectaccount.com', + }); + const clone = jwt.createScoped('hi'); + assert.strictEqual('hi', clone.scopes); + }); + + it('createScoped should handle nulls', () => { + const jwt = new JWT(); + const clone = jwt.createScoped('hi'); + assert.strictEqual(jwt.email, undefined); + assert.strictEqual(jwt.keyFile, undefined); + assert.strictEqual(jwt.key, undefined); + assert.strictEqual(jwt.subject, undefined); + assert.strictEqual('hi', clone.scopes); + }); + + it('createScoped should not return the original instance', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + const clone = jwt.createScoped('hi'); + assert.notStrictEqual(jwt, clone); + }); + + it('createScopedRequired should return true when scopes is null', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + subject: 'bar@subjectaccount.com', + }); + // tslint:disable-next-line deprecation + assert.strictEqual(true, jwt.createScopedRequired()); + }); + + it('createScopedRequired should return true when scopes is an empty array', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: [], + subject: 'bar@subjectaccount.com', + }); + // tslint:disable-next-line deprecation + assert.strictEqual(true, jwt.createScopedRequired()); + }); + + it('createScopedRequired should return true when scopes is an empty string', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: '', + subject: 'bar@subjectaccount.com', + }); + // tslint:disable-next-line deprecation + assert.strictEqual(true, jwt.createScopedRequired()); + }); + + it('createScopedRequired should return false when scopes is a filled-in string', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: 'http://foo', + subject: 'bar@subjectaccount.com', + }); + // tslint:disable-next-line deprecation + assert.strictEqual(false, jwt.createScopedRequired()); + }); + + it('createScopedRequired should return false when scopes is a filled-in array', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); + + // tslint:disable-next-line deprecation + assert.strictEqual(false, jwt.createScopedRequired()); + }); + + it('createScopedRequired should return false when scopes is not an array or a string, but can be used as a string', () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: '/path/to/key.pem', + scopes: '2', + subject: 'bar@subjectaccount.com', + }); + // tslint:disable-next-line deprecation + assert.strictEqual(false, jwt.createScopedRequired()); + }); + + it('fromJson should error on null json', () => { + assert.throws(() => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (jwt as any).fromJSON(null); + }); + }); + + it('fromJson should error on empty json', () => { + assert.throws(() => { + jwt.fromJSON({}); + }); + }); + + it('fromJson should error on missing client_email', () => { + delete json.client_email; + assert.throws(() => { + jwt.fromJSON(json); + }); + }); + + it('fromJson should error on missing private_key', () => { + delete json.private_key; + assert.throws(() => { + jwt.fromJSON(json); + }); + }); + + it('fromJson should create JWT with client_email', () => { + jwt.fromJSON(json); + assert.strictEqual(json.client_email, jwt.email); }); - const clone = jwt.createScoped(); - assert.strictEqual(undefined, clone.scopes); -}); -it('createScoped should set scope when scope was null', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - subject: 'bar@subjectaccount.com', + it('fromJson should create JWT with private_key', () => { + jwt.fromJSON(json); + assert.strictEqual(json.private_key, jwt.key); }); - const clone = jwt.createScoped('hi'); - assert.strictEqual('hi', clone.scopes); -}); -it('createScoped should handle nulls', () => { - const jwt = new JWT(); - const clone = jwt.createScoped('hi'); - assert.strictEqual(jwt.email, undefined); - assert.strictEqual(jwt.keyFile, undefined); - assert.strictEqual(jwt.key, undefined); - assert.strictEqual(jwt.subject, undefined); - assert.strictEqual('hi', clone.scopes); -}); - -it('createScoped should not return the original instance', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', + it('fromJson should create JWT with private_key_id', () => { + jwt.fromJSON(json); + assert.strictEqual(json.private_key_id, jwt.keyId); }); - const clone = jwt.createScoped('hi'); - assert.notStrictEqual(jwt, clone); -}); -it('createScopedRequired should return true when scopes is null', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - subject: 'bar@subjectaccount.com', + it('fromJson should create JWT with null scopes', () => { + jwt.fromJSON(json); + assert.strictEqual(undefined, jwt.scopes); }); - // tslint:disable-next-line deprecation - assert.strictEqual(true, jwt.createScopedRequired()); -}); -it('createScopedRequired should return true when scopes is an empty array', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: [], - subject: 'bar@subjectaccount.com', + it('fromJson should create JWT with null subject', () => { + jwt.fromJSON(json); + assert.strictEqual(undefined, jwt.subject); }); - // tslint:disable-next-line deprecation - assert.strictEqual(true, jwt.createScopedRequired()); -}); -it('createScopedRequired should return true when scopes is an empty string', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: '', - subject: 'bar@subjectaccount.com', + it('fromJson should create JWT with null keyFile', () => { + jwt.fromJSON(json); + assert.strictEqual(undefined, jwt.keyFile); }); - // tslint:disable-next-line deprecation - assert.strictEqual(true, jwt.createScopedRequired()); -}); -it('createScopedRequired should return false when scopes is a filled-in string', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: 'http://foo', - subject: 'bar@subjectaccount.com', + it('should error on missing client_id', () => { + const json = createRefreshJSON(); + delete json.client_id; + const jwt = new JWT(); + assert.throws(() => { + jwt.fromJSON(json); + }); }); - // tslint:disable-next-line deprecation - assert.strictEqual(false, jwt.createScopedRequired()); -}); -it('createScopedRequired should return false when scopes is a filled-in array', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', + it('should error on missing client_secret', () => { + const json = createRefreshJSON(); + delete json.client_secret; + const jwt = new JWT(); + assert.throws(() => { + jwt.fromJSON(json); + }); }); - // tslint:disable-next-line deprecation - assert.strictEqual(false, jwt.createScopedRequired()); -}); - -it('createScopedRequired should return false when scopes is not an array or a string, but can be used as a string', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: '2', - subject: 'bar@subjectaccount.com', + it('should error on missing refresh_token', () => { + const json = createRefreshJSON(); + delete json.refresh_token; + const jwt = new JWT(); + assert.throws(() => { + jwt.fromJSON(json); + }); }); - // tslint:disable-next-line deprecation - assert.strictEqual(false, jwt.createScopedRequired()); -}); -it('fromJson should error on null json', () => { - assert.throws(() => { + it('fromStream should error on null stream', done => { // Test verifies invalid parameter tests, which requires cast to any. // tslint:disable-next-line no-any - (jwt as any).fromJSON(null); - }); -}); - -it('fromJson should error on empty json', () => { - assert.throws(() => { - jwt.fromJSON({}); - }); -}); - -it('fromJson should error on missing client_email', () => { - delete json.client_email; - assert.throws(() => { - jwt.fromJSON(json); - }); -}); - -it('fromJson should error on missing private_key', () => { - delete json.private_key; - assert.throws(() => { - jwt.fromJSON(json); + (jwt as any).fromStream(null, (err: Error) => { + assert.strictEqual(true, err instanceof Error); + done(); + }); }); -}); - -it('fromJson should create JWT with client_email', () => { - jwt.fromJSON(json); - assert.strictEqual(json.client_email, jwt.email); -}); -it('fromJson should create JWT with private_key', () => { - jwt.fromJSON(json); - assert.strictEqual(json.private_key, jwt.key); -}); - -it('fromJson should create JWT with private_key_id', () => { - jwt.fromJSON(json); - assert.strictEqual(json.private_key_id, jwt.keyId); -}); - -it('fromJson should create JWT with null scopes', () => { - jwt.fromJSON(json); - assert.strictEqual(undefined, jwt.scopes); -}); - -it('fromJson should create JWT with null subject', () => { - jwt.fromJSON(json); - assert.strictEqual(undefined, jwt.subject); -}); - -it('fromJson should create JWT with null keyFile', () => { - jwt.fromJSON(json); - assert.strictEqual(undefined, jwt.keyFile); -}); - -it('should error on missing client_id', () => { - const json = createRefreshJSON(); - delete json.client_id; - const jwt = new JWT(); - assert.throws(() => { - jwt.fromJSON(json); + it('fromStream should read the stream and create a jwt', done => { + // Read the contents of the file into a json object. + const fileContents = fs.readFileSync( + './test/fixtures/private.json', + 'utf-8' + ); + const json = JSON.parse(fileContents); + + // Now open a stream on the same file. + const stream = fs.createReadStream('./test/fixtures/private.json'); + + // And pass it into the fromStream method. + jwt.fromStream(stream, err => { + assert.strictEqual(undefined, err); + // Ensure that the correct bits were pulled from the stream. + assert.strictEqual(json.private_key, jwt.key); + assert.strictEqual(json.private_key_id, jwt.keyId); + assert.strictEqual(json.client_email, jwt.email); + assert.strictEqual(undefined, jwt.keyFile); + assert.strictEqual(undefined, jwt.subject); + assert.strictEqual(undefined, jwt.scopes); + done(); + }); }); -}); -it('should error on missing client_secret', () => { - const json = createRefreshJSON(); - delete json.client_secret; - const jwt = new JWT(); - assert.throws(() => { - jwt.fromJSON(json); + it('fromAPIKey should error without api key', () => { + assert.throws(() => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (jwt as any).fromAPIKey(undefined); + }); }); -}); -it('should error on missing refresh_token', () => { - const json = createRefreshJSON(); - delete json.refresh_token; - const jwt = new JWT(); - assert.throws(() => { - jwt.fromJSON(json); + it('fromAPIKey should error with invalid api key type', () => { + const KEY = 'test'; + assert.throws(() => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + jwt.fromAPIKey({key: KEY} as any); + }); }); -}); -it('fromStream should error on null stream', done => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (jwt as any).fromStream(null, (err: Error) => { - assert.strictEqual(true, err instanceof Error); - done(); + it('fromAPIKey should set the .apiKey property on the instance', () => { + const KEY = 'test'; + jwt.fromAPIKey(KEY); + assert.strictEqual(jwt.apiKey, KEY); }); -}); - -it('fromStream should read the stream and create a jwt', done => { - // Read the contents of the file into a json object. - const fileContents = fs.readFileSync('./test/fixtures/private.json', 'utf-8'); - const json = JSON.parse(fileContents); - // Now open a stream on the same file. - const stream = fs.createReadStream('./test/fixtures/private.json'); - - // And pass it into the fromStream method. - jwt.fromStream(stream, err => { - assert.strictEqual(undefined, err); - // Ensure that the correct bits were pulled from the stream. - assert.strictEqual(json.private_key, jwt.key); - assert.strictEqual(json.private_key_id, jwt.keyId); - assert.strictEqual(json.client_email, jwt.email); - assert.strictEqual(undefined, jwt.keyFile); - assert.strictEqual(undefined, jwt.subject); - assert.strictEqual(undefined, jwt.scopes); - done(); + it('getCredentials should handle a key', async () => { + const jwt = new JWT({key: PEM_CONTENTS}); + const {private_key} = await jwt.getCredentials(); + assert.strictEqual(private_key, PEM_CONTENTS); }); -}); -it('fromAPIKey should error without api key', () => { - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (jwt as any).fromAPIKey(undefined); + it('getCredentials should handle a p12 keyFile', async () => { + const jwt = new JWT({keyFile: P12_PATH}); + const {private_key, client_email} = await jwt.getCredentials(); + assert(private_key); + assert.strictEqual(client_email, undefined); }); -}); -it('fromAPIKey should error with invalid api key type', () => { - const KEY = 'test'; - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - jwt.fromAPIKey({key: KEY} as any); + it('getCredentials should handle a json keyFile', async () => { + const jwt = new JWT(); + jwt.fromJSON(json); + const {private_key, client_email} = await jwt.getCredentials(); + assert.strictEqual(private_key, json.private_key); + assert.strictEqual(client_email, json.client_email); + }); + + it('getRequestHeaders populates x-goog-user-project for JWT client', async () => { + const auth = new GoogleAuth({ + credentials: Object.assign( + require('../../test/fixtures/service-account-with-quota.json'), + { + private_key: keypair(1024 /* bitsize of private key */).private, + } + ), + }); + const client = await auth.getClient(); + assert(client instanceof JWT); + // If a URL isn't provided to authorize, the OAuth2Client super class is + // executed, which was already exercised. + const headers = await client.getRequestHeaders( + 'http:/example.com/my_test_service' + ); + assert.strictEqual(headers['x-goog-user-project'], 'fake-quota-project'); }); -}); - -it('fromAPIKey should set the .apiKey property on the instance', () => { - const KEY = 'test'; - jwt.fromAPIKey(KEY); - assert.strictEqual(jwt.apiKey, KEY); -}); -it('getCredentials should handle a key', async () => { - const jwt = new JWT({key: PEM_CONTENTS}); - const {private_key} = await jwt.getCredentials(); - assert.strictEqual(private_key, PEM_CONTENTS); -}); + it('should return an ID token for fetchIdToken', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); -it('getCredentials should handle a p12 keyFile', async () => { - const jwt = new JWT({keyFile: P12_PATH}); - const {private_key, client_email} = await jwt.getCredentials(); - assert(private_key); - assert.strictEqual(client_email, undefined); -}); - -it('getCredentials should handle a json keyFile', async () => { - const jwt = new JWT(); - jwt.fromJSON(json); - const {private_key, client_email} = await jwt.getCredentials(); - assert.strictEqual(private_key, json.private_key); - assert.strictEqual(client_email, json.client_email); -}); - -it('getRequestHeaders populates x-goog-user-project for JWT client', async () => { - const auth = new GoogleAuth({ - credentials: Object.assign( - require('../../test/fixtures/service-account-with-quota.json'), - { - private_key: keypair(1024 /* bitsize of private key */).private, - } - ), - }); - const client = await auth.getClient(); - assert(client instanceof JWT); - // If a URL isn't provided to authorize, the OAuth2Client super class is - // executed, which was already exercised. - const headers = await client.getRequestHeaders( - 'http:/example.com/my_test_service' - ); - assert.strictEqual(headers['x-goog-user-project'], 'fake-quota-project'); -}); - -it('should return an ID token for fetchIdToken', async () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: PEM_CONTENTS, - subject: 'ignored@subjectaccount.com', + const scope = createGTokenMock({id_token: 'abc123'}); + const idtoken = await jwt.fetchIdToken('a-target-audience'); + scope.done(); + assert.strictEqual(idtoken, 'abc123'); }); - const scope = createGTokenMock({id_token: 'abc123'}); - const idtoken = await jwt.fetchIdToken('a-target-audience'); - scope.done(); - assert.strictEqual(idtoken, 'abc123'); -}); + it('should throw an error if ID token is not set', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: PEM_CONTENTS, + subject: 'ignored@subjectaccount.com', + }); -it('should throw an error if ID token is not set', async () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: PEM_CONTENTS, - subject: 'ignored@subjectaccount.com', + const scope = createGTokenMock({access_token: 'a-token'}); + try { + await jwt.fetchIdToken('a-target-audience'); + } catch { + scope.done(); + return; + } + assert.fail('failed to throw'); }); - - const scope = createGTokenMock({access_token: 'a-token'}); - try { - await jwt.fetchIdToken('a-target-audience'); - } catch { - scope.done(); - return; - } - assert.fail('failed to throw'); }); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index e7728e5c..c074eeed 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {describe, it, beforeEach, afterEach} from 'mocha'; import * as fs from 'fs'; import * as jws from 'jws'; import * as sinon from 'sinon'; @@ -23,167 +23,172 @@ import * as messages from '../src/messages'; const keypair = require('keypair'); -// Creates a standard JSON credentials object for testing. -const json = { - private_key_id: 'key123', - private_key: 'privatekey', - client_email: 'hello@youarecool.com', - client_id: 'client123', - type: 'service_account', -}; - -const keys = keypair(1024 /* bitsize of private key */); -const testUri = 'http:/example.com/my_test_service'; -const email = 'foo@serviceaccount.com'; - -let client: JWTAccess; -let sandbox: sinon.SinonSandbox; -beforeEach(() => { - client = new JWTAccess(); - sandbox = sinon.createSandbox(); -}); -afterEach(() => { - sandbox.restore(); -}); - -it('should emit warning for createScopedRequired', () => { - const stub = sandbox.stub(process, 'emitWarning'); - // tslint:disable-next-line deprecation - client.createScopedRequired(); - assert(stub.called); -}); +describe('jwtaccess', () => { + // Creates a standard JSON credentials object for testing. + const json = { + private_key_id: 'key123', + private_key: 'privatekey', + client_email: 'hello@youarecool.com', + client_id: 'client123', + type: 'service_account', + }; + + const keys = keypair(1024 /* bitsize of private key */); + const testUri = 'http:/example.com/my_test_service'; + const email = 'foo@serviceaccount.com'; + + let client: JWTAccess; + let sandbox: sinon.SinonSandbox; + beforeEach(() => { + client = new JWTAccess(); + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + }); -it('getRequestHeaders should create a signed JWT token as the access token', () => { - const client = new JWTAccess(email, keys.private); - const headers = client.getRequestHeaders(testUri); - assert.notStrictEqual(null, headers, 'an creds object should be present'); - const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); - assert.deepStrictEqual({alg: 'RS256', typ: 'JWT'}, decoded.header); - const payload = decoded.payload; - assert.strictEqual(email, payload.iss); - assert.strictEqual(email, payload.sub); - assert.strictEqual(testUri, payload.aud); -}); + it('should emit warning for createScopedRequired', () => { + const stub = sandbox.stub(process, 'emitWarning'); + // tslint:disable-next-line deprecation + client.createScopedRequired(); + assert(stub.called); + }); -it('getRequestHeaders should set key id in header when available', () => { - const client = new JWTAccess(email, keys.private, '101'); - const headers = client.getRequestHeaders(testUri); - const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); - assert.deepStrictEqual( - {alg: 'RS256', typ: 'JWT', kid: '101'}, - decoded.header - ); -}); + it('getRequestHeaders should create a signed JWT token as the access token', () => { + const client = new JWTAccess(email, keys.private); + const headers = client.getRequestHeaders(testUri); + assert.notStrictEqual(null, headers, 'an creds object should be present'); + const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + assert.deepStrictEqual({alg: 'RS256', typ: 'JWT'}, decoded.header); + const payload = decoded.payload; + assert.strictEqual(email, payload.iss); + assert.strictEqual(email, payload.sub); + assert.strictEqual(testUri, payload.aud); + }); -it('getRequestHeaders should not allow overriding with additionalClaims', () => { - const client = new JWTAccess(email, keys.private); - const additionalClaims = {iss: 'not-the-email'}; - assert.throws(() => { - client.getRequestHeaders(testUri, additionalClaims); - }, /^Error: The 'iss' property is not allowed when passing additionalClaims. This claim is included in the JWT by default.$/); -}); + it('getRequestHeaders should set key id in header when available', () => { + const client = new JWTAccess(email, keys.private, '101'); + const headers = client.getRequestHeaders(testUri); + const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + assert.deepStrictEqual( + {alg: 'RS256', typ: 'JWT', kid: '101'}, + decoded.header + ); + }); -it('getRequestHeaders should return a cached token on the second request', () => { - const client = new JWTAccess(email, keys.private); - const res = client.getRequestHeaders(testUri); - const res2 = client.getRequestHeaders(testUri); - assert.strictEqual(res, res2); -}); + it('getRequestHeaders should not allow overriding with additionalClaims', () => { + const client = new JWTAccess(email, keys.private); + const additionalClaims = {iss: 'not-the-email'}; + assert.throws(() => { + client.getRequestHeaders(testUri, additionalClaims); + }, /^Error: The 'iss' property is not allowed when passing additionalClaims. This claim is included in the JWT by default.$/); + }); -it('getRequestHeaders should not return cached tokens older than an hour', () => { - const client = new JWTAccess(email, keys.private); - const res = client.getRequestHeaders(testUri); - const realDateNow = Date.now; - try { - // go forward in time one hour (plus a little) - Date.now = () => realDateNow() + 1000 * 60 * 60 + 10; + it('getRequestHeaders should return a cached token on the second request', () => { + const client = new JWTAccess(email, keys.private); + const res = client.getRequestHeaders(testUri); const res2 = client.getRequestHeaders(testUri); - assert.notStrictEqual(res, res2); - } finally { - // return date.now to it's normally scheduled programming - Date.now = realDateNow; - } -}); - -it('createScopedRequired should return false', () => { - const client = new JWTAccess('foo@serviceaccount.com', null); - // tslint:disable-next-line deprecation - assert.strictEqual(false, client.createScopedRequired()); -}); + assert.strictEqual(res, res2); + }); -it('fromJson should error on null json', () => { - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (client as any).fromJSON(null); + it('getRequestHeaders should not return cached tokens older than an hour', () => { + const client = new JWTAccess(email, keys.private); + const res = client.getRequestHeaders(testUri); + const realDateNow = Date.now; + try { + // go forward in time one hour (plus a little) + Date.now = () => realDateNow() + 1000 * 60 * 60 + 10; + const res2 = client.getRequestHeaders(testUri); + assert.notStrictEqual(res, res2); + } finally { + // return date.now to it's normally scheduled programming + Date.now = realDateNow; + } }); -}); -it('fromJson should error on empty json', () => { - assert.throws(() => { - client.fromJSON({}); + it('createScopedRequired should return false', () => { + const client = new JWTAccess('foo@serviceaccount.com', null); + // tslint:disable-next-line deprecation + assert.strictEqual(false, client.createScopedRequired()); }); -}); -it('fromJson should error on missing client_email', () => { - const j = Object.assign({}, json); - delete j.client_email; - assert.throws(() => { - client.fromJSON(j); + it('fromJson should error on null json', () => { + assert.throws(() => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (client as any).fromJSON(null); + }); }); -}); -it('fromJson should error on missing private_key', () => { - const j = Object.assign({}, json); - delete j.private_key; - assert.throws(() => { - client.fromJSON(j); + it('fromJson should error on empty json', () => { + assert.throws(() => { + client.fromJSON({}); + }); }); -}); -it('fromJson should create JWT with client_email', () => { - client.fromJSON(json); - assert.strictEqual(json.client_email, client.email); -}); + it('fromJson should error on missing client_email', () => { + const j = Object.assign({}, json); + delete j.client_email; + assert.throws(() => { + client.fromJSON(j); + }); + }); -it('fromJson should create JWT with private_key', () => { - client.fromJSON(json); - assert.strictEqual(json.private_key, client.key); -}); + it('fromJson should error on missing private_key', () => { + const j = Object.assign({}, json); + delete j.private_key; + assert.throws(() => { + client.fromJSON(j); + }); + }); -it('fromJson should create JWT with private_key_id', () => { - client.fromJSON(json); - assert.strictEqual(json.private_key_id, client.keyId); -}); + it('fromJson should create JWT with client_email', () => { + client.fromJSON(json); + assert.strictEqual(json.client_email, client.email); + }); -it('fromStream should error on null stream', done => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (client as any).fromStream(null, (err: Error) => { - assert.strictEqual(true, err instanceof Error); - done(); + it('fromJson should create JWT with private_key', () => { + client.fromJSON(json); + assert.strictEqual(json.private_key, client.key); }); -}); -it('fromStream should construct a JWT Header instance from a stream', async () => { - // Read the contents of the file into a json object. - const fileContents = fs.readFileSync('./test/fixtures/private.json', 'utf-8'); - const json = JSON.parse(fileContents); + it('fromJson should create JWT with private_key_id', () => { + client.fromJSON(json); + assert.strictEqual(json.private_key_id, client.keyId); + }); - // Now open a stream on the same file. - const stream = fs.createReadStream('./test/fixtures/private.json'); + it('fromStream should error on null stream', done => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (client as any).fromStream(null, (err: Error) => { + assert.strictEqual(true, err instanceof Error); + done(); + }); + }); - // And pass it into the fromStream method. - await client.fromStream(stream); - // Ensure that the correct bits were pulled from the stream. - assert.strictEqual(json.private_key, client.key); - assert.strictEqual(json.client_email, client.email); -}); + it('fromStream should construct a JWT Header instance from a stream', async () => { + // Read the contents of the file into a json object. + const fileContents = fs.readFileSync( + './test/fixtures/private.json', + 'utf-8' + ); + const json = JSON.parse(fileContents); + + // Now open a stream on the same file. + const stream = fs.createReadStream('./test/fixtures/private.json'); + + // And pass it into the fromStream method. + await client.fromStream(stream); + // Ensure that the correct bits were pulled from the stream. + assert.strictEqual(json.private_key, client.key); + assert.strictEqual(json.client_email, client.email); + }); -it('should warn about deprecation of getRequestMetadata', () => { - const client = new JWTAccess(email, keys.private); - const stub = sandbox.stub(messages, 'warn'); - // tslint:disable-next-line deprecation - client.getRequestMetadata(testUri); - assert.strictEqual(stub.calledOnce, true); + it('should warn about deprecation of getRequestMetadata', () => { + const client = new JWTAccess(email, keys.private); + const stub = sandbox.stub(messages, 'warn'); + // tslint:disable-next-line deprecation + client.getRequestMetadata(testUri); + assert.strictEqual(stub.calledOnce, true); + }); }); diff --git a/test/test.loginticket.ts b/test/test.loginticket.ts index 757886a4..38aa34a1 100644 --- a/test/test.loginticket.ts +++ b/test/test.loginticket.ts @@ -16,27 +16,29 @@ import * as assert from 'assert'; import {describe, it} from 'mocha'; import {LoginTicket} from '../src/auth/loginticket'; -it('should return null userId even if no payload', () => { - const ticket = new LoginTicket(); - assert.strictEqual(ticket.getUserId(), null); -}); +describe('loginticket', () => { + it('should return null userId even if no payload', () => { + const ticket = new LoginTicket(); + assert.strictEqual(ticket.getUserId(), null); + }); -it('should return envelope', () => { - const ticket = new LoginTicket('myenvelope'); - assert.strictEqual(ticket.getEnvelope(), 'myenvelope'); -}); + it('should return envelope', () => { + const ticket = new LoginTicket('myenvelope'); + assert.strictEqual(ticket.getEnvelope(), 'myenvelope'); + }); -it('should return attributes from getAttributes', () => { - const payload = { - aud: 'aud', - sub: 'sub', - iss: 'iss', - iat: 1514162443, - exp: 1514166043, - }; - const ticket = new LoginTicket('myenvelope', payload); - assert.deepStrictEqual(ticket.getAttributes(), { - envelope: 'myenvelope', - payload, + it('should return attributes from getAttributes', () => { + const payload = { + aud: 'aud', + sub: 'sub', + iss: 'iss', + iat: 1514162443, + exp: 1514166043, + }; + const ticket = new LoginTicket('myenvelope', payload); + assert.deepStrictEqual(ticket.getAttributes(), { + envelope: 'myenvelope', + payload, + }); }); }); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 81d8db23..850594e8 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {describe, it, beforeEach, afterEach} from 'mocha'; const assertRejects = require('assert-rejects'); import * as crypto from 'crypto'; import * as fs from 'fs'; @@ -30,1273 +30,1303 @@ import * as messages from '../src/messages'; nock.disableNetConnect(); -const CLIENT_ID = 'CLIENT_ID'; -const CLIENT_SECRET = 'CLIENT_SECRET'; -const REDIRECT_URI = 'REDIRECT'; -const ACCESS_TYPE = 'offline'; -const SCOPE = 'scopex'; -const SCOPE_ARRAY = ['scopex', 'scopey']; -const publicKey = fs.readFileSync('./test/fixtures/public.pem', 'utf-8'); -const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); -const baseUrl = 'https://oauth2.googleapis.com'; -const certsPath = '/oauth2/v1/certs'; -const certsResPath = path.join( - __dirname, - '../../test/fixtures/oauthcertspem.json' -); - -describe(__filename, () => { - let client: OAuth2Client; - let sandbox: sinon.SinonSandbox; - beforeEach(() => { - client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - nock.cleanAll(); - sandbox.restore(); - }); +describe('oauth2', () => { + const CLIENT_ID = 'CLIENT_ID'; + const CLIENT_SECRET = 'CLIENT_SECRET'; + const REDIRECT_URI = 'REDIRECT'; + const ACCESS_TYPE = 'offline'; + const SCOPE = 'scopex'; + const SCOPE_ARRAY = ['scopex', 'scopey']; + const publicKey = fs.readFileSync('./test/fixtures/public.pem', 'utf-8'); + const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); + const baseUrl = 'https://oauth2.googleapis.com'; + const certsPath = '/oauth2/v1/certs'; + const certsResPath = path.join( + __dirname, + '../../test/fixtures/oauthcertspem.json' + ); + + describe(__filename, () => { + let client: OAuth2Client; + let sandbox: sinon.SinonSandbox; + beforeEach(() => { + client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); + sandbox = sinon.createSandbox(); + }); - it('should generate a valid consent page url', done => { - const opts = { - access_type: ACCESS_TYPE, - scope: SCOPE, - response_type: 'code token', - }; - - const oauth2client = new OAuth2Client({ - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - redirectUri: REDIRECT_URI, + afterEach(() => { + nock.cleanAll(); + sandbox.restore(); }); - const generated = oauth2client.generateAuthUrl(opts); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.response_type, 'code token'); - assert.strictEqual(query.access_type, ACCESS_TYPE); - assert.strictEqual(query.scope, SCOPE); - assert.strictEqual(query.client_id, CLIENT_ID); - assert.strictEqual(query.redirect_uri, REDIRECT_URI); - done(); - }); + it('should generate a valid consent page url', done => { + const opts = { + access_type: ACCESS_TYPE, + scope: SCOPE, + response_type: 'code token', + }; - it('should throw an error if generateAuthUrl is called with invalid parameters', () => { - const opts = { - access_type: ACCESS_TYPE, - scope: SCOPE, - code_challenge_method: CodeChallengeMethod.S256, - }; - assert.throws( - () => client.generateAuthUrl(opts), - /If a code_challenge_method is provided, code_challenge must be included/ - ); - }); + const oauth2client = new OAuth2Client({ + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + redirectUri: REDIRECT_URI, + }); - it('should generate a valid code verifier and resulting challenge', async () => { - const codes = await client.generateCodeVerifierAsync(); - // ensure the code_verifier matches all requirements - assert.strictEqual(codes.codeVerifier.length, 128); - const match = codes.codeVerifier.match(/[a-zA-Z0-9\-\.~_]*/); - assert(match); - if (!match) return; - assert(match.length > 0 && match[0] === codes.codeVerifier); - }); + const generated = oauth2client.generateAuthUrl(opts); + const parsed = url.parse(generated); + if (typeof parsed.query !== 'string') { + throw new Error('Unable to parse querystring'); + } + const query = qs.parse(parsed.query); + assert.strictEqual(query.response_type, 'code token'); + assert.strictEqual(query.access_type, ACCESS_TYPE); + assert.strictEqual(query.scope, SCOPE); + assert.strictEqual(query.client_id, CLIENT_ID); + assert.strictEqual(query.redirect_uri, REDIRECT_URI); + done(); + }); - it('should include code challenge and method in the url', async () => { - const codes = await client.generateCodeVerifierAsync(); - const authUrl = client.generateAuthUrl({ - code_challenge: codes.codeChallenge, - code_challenge_method: CodeChallengeMethod.S256, + it('should throw an error if generateAuthUrl is called with invalid parameters', () => { + const opts = { + access_type: ACCESS_TYPE, + scope: SCOPE, + code_challenge_method: CodeChallengeMethod.S256, + }; + assert.throws( + () => client.generateAuthUrl(opts), + /If a code_challenge_method is provided, code_challenge must be included/ + ); }); - const parsed = url.parse(authUrl); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const props = qs.parse(parsed.query); - assert.strictEqual(props.code_challenge, codes.codeChallenge); - assert.strictEqual(props.code_challenge_method, CodeChallengeMethod.S256); - }); - it('should verifyIdToken properly', async () => { - const fakeCerts = {a: 'a', b: 'b'}; - const idToken = 'idToken'; - const audience = 'fakeAudience'; - const maxExpiry = 5; - const payload = { - aud: 'aud', - sub: 'sub', - iss: 'iss', - iat: 1514162443, - exp: 1514166043, - }; - const scope = nock('https://www.googleapis.com') - .get('/oauth2/v1/certs') - .reply(200, fakeCerts); - client.verifySignedJwtWithCertsAsync = async ( - jwt: string, - certs: {}, - requiredAudience: string | string[], - issuers?: string[], - theMaxExpiry?: number - ) => { - assert.strictEqual(jwt, idToken); - assert.deepStrictEqual(certs, fakeCerts); - assert.strictEqual(requiredAudience, audience); - assert.strictEqual(theMaxExpiry, maxExpiry); - return new LoginTicket('c', payload); - }; - const result = await client.verifyIdToken({idToken, audience, maxExpiry}); - scope.done(); - assert.notStrictEqual(result, null); - if (result) { - assert.strictEqual(result.getEnvelope(), 'c'); - assert.strictEqual(result.getPayload(), payload); - } - }); + it('should generate a valid code verifier and resulting challenge', async () => { + const codes = await client.generateCodeVerifierAsync(); + // ensure the code_verifier matches all requirements + assert.strictEqual(codes.codeVerifier.length, 128); + const match = codes.codeVerifier.match(/[a-zA-Z0-9\-\.~_]*/); + assert(match); + if (!match) return; + assert(match.length > 0 && match[0] === codes.codeVerifier); + }); - it('should provide a reasonable error in verifyIdToken with wrong parameters', async () => { - const fakeCerts = {a: 'a', b: 'b'}; - const idToken = 'idToken'; - const audience = 'fakeAudience'; - const payload = { - aud: 'aud', - sub: 'sub', - iss: 'iss', - iat: 1514162443, - exp: 1514166043, - }; - client.verifySignedJwtWithCertsAsync = async ( - jwt: string, - certs: {}, - requiredAudience: string - ) => { - assert.strictEqual(jwt, idToken); - assert.deepStrictEqual(certs, fakeCerts); - assert.strictEqual(requiredAudience, audience); - return new LoginTicket('c', payload); - }; - assert.throws( - // tslint:disable-next-line no-any - () => (client as any).verifyIdToken(idToken, audience), - /This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry./ - ); - }); + it('should include code challenge and method in the url', async () => { + const codes = await client.generateCodeVerifierAsync(); + const authUrl = client.generateAuthUrl({ + code_challenge: codes.codeChallenge, + code_challenge_method: CodeChallengeMethod.S256, + }); + const parsed = url.parse(authUrl); + if (typeof parsed.query !== 'string') { + throw new Error('Unable to parse querystring'); + } + const props = qs.parse(parsed.query); + assert.strictEqual(props.code_challenge, codes.codeChallenge); + assert.strictEqual(props.code_challenge_method, CodeChallengeMethod.S256); + }); - it('should allow scopes to be specified as array', () => { - const opts = { - access_type: ACCESS_TYPE, - scope: SCOPE_ARRAY, - response_type: 'code token', - }; - const generated = client.generateAuthUrl(opts); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.scope, SCOPE_ARRAY.join(' ')); - }); + it('should verifyIdToken properly', async () => { + const fakeCerts = {a: 'a', b: 'b'}; + const idToken = 'idToken'; + const audience = 'fakeAudience'; + const maxExpiry = 5; + const payload = { + aud: 'aud', + sub: 'sub', + iss: 'iss', + iat: 1514162443, + exp: 1514166043, + }; + const scope = nock('https://www.googleapis.com') + .get('/oauth2/v1/certs') + .reply(200, fakeCerts); + client.verifySignedJwtWithCertsAsync = async ( + jwt: string, + certs: {}, + requiredAudience: string | string[], + issuers?: string[], + theMaxExpiry?: number + ) => { + assert.strictEqual(jwt, idToken); + assert.deepStrictEqual(certs, fakeCerts); + assert.strictEqual(requiredAudience, audience); + assert.strictEqual(theMaxExpiry, maxExpiry); + return new LoginTicket('c', payload); + }; + const result = await client.verifyIdToken({idToken, audience, maxExpiry}); + scope.done(); + assert.notStrictEqual(result, null); + if (result) { + assert.strictEqual(result.getEnvelope(), 'c'); + assert.strictEqual(result.getPayload(), payload); + } + }); - it('should set response_type param to code if none is given while generating the consent page url', () => { - const generated = client.generateAuthUrl(); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.response_type, 'code'); - }); + it('should provide a reasonable error in verifyIdToken with wrong parameters', async () => { + const fakeCerts = {a: 'a', b: 'b'}; + const idToken = 'idToken'; + const audience = 'fakeAudience'; + const payload = { + aud: 'aud', + sub: 'sub', + iss: 'iss', + iat: 1514162443, + exp: 1514166043, + }; + client.verifySignedJwtWithCertsAsync = async ( + jwt: string, + certs: {}, + requiredAudience: string + ) => { + assert.strictEqual(jwt, idToken); + assert.deepStrictEqual(certs, fakeCerts); + assert.strictEqual(requiredAudience, audience); + return new LoginTicket('c', payload); + }; + assert.throws( + // tslint:disable-next-line no-any + () => (client as any).verifyIdToken(idToken, audience), + /This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry./ + ); + }); - it('should verify a valid certificate against a jwt', async () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = JSON.stringify({kid: 'keyid', alg: 'RS256'}); - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - const login = await client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ); - assert.strictEqual(login.getUserId(), '123456789'); - }); + it('should allow scopes to be specified as array', () => { + const opts = { + access_type: ACCESS_TYPE, + scope: SCOPE_ARRAY, + response_type: 'code token', + }; + const generated = client.generateAuthUrl(opts); + const parsed = url.parse(generated); + if (typeof parsed.query !== 'string') { + throw new Error('Unable to parse querystring'); + } + const query = qs.parse(parsed.query); + assert.strictEqual(query.scope, SCOPE_ARRAY.join(' ')); + }); + + it('should set response_type param to code if none is given while generating the consent page url', () => { + const generated = client.generateAuthUrl(); + const parsed = url.parse(generated); + if (typeof parsed.query !== 'string') { + throw new Error('Unable to parse querystring'); + } + const query = qs.parse(parsed.query); + assert.strictEqual(query.response_type, 'code'); + }); - it('should fail due to invalid audience', () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"wrongaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( + it('should verify a valid certificate against a jwt', async () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = JSON.stringify({kid: 'keyid', alg: 'RS256'}); + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + const login = await client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, 'testaudience' - ), - /Wrong recipient/ - ); - }); + ); + assert.strictEqual(login.getUserId(), '123456789'); + }); - it('should fail due to invalid array of audiences', () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"wrongaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - const validAudiences = ['testaudience', 'extra-audience']; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - validAudiences - ), - /Wrong recipient/ - ); - }); + it('should fail due to invalid audience', () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"wrongaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /Wrong recipient/ + ); + }); - it('should fail due to invalid signature', () => { - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":1393241597,' + - '"exp":1393245497' + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - // Originally: data += '.'+signature; - data += signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ), - /Wrong number of segments/ - ); - }); + it('should fail due to invalid array of audiences', () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"wrongaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + const validAudiences = ['testaudience', 'extra-audience']; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + validAudiences + ), + /Wrong recipient/ + ); + }); - it('should fail due to invalid envelope', () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid"' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ), - /Can\'t parse token envelope/ - ); - }); + it('should fail due to invalid signature', () => { + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":1393241597,' + + '"exp":1393245497' + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + // Originally: data += '.'+signature; + data += signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /Wrong number of segments/ + ); + }); - it('should fail due to invalid payload', () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"testissuer"' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ), - /Can\'t parse token payload/ - ); - }); + it('should fail due to invalid envelope', () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid"' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /Can\'t parse token envelope/ + ); + }); - it('should fail due to invalid signature', () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - const data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64') + - '.' + - 'broken-signature'; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ), - /Invalid token signature/ - ); - }); + it('should fail due to invalid payload', () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer"' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /Can\'t parse token payload/ + ); + }); - it('should fail due to no expiration date', () => { - const now = new Date().getTime() / 1000; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ), - /No expiration time/ - ); - }); + it('should fail due to invalid signature', () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + const data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64') + + '.' + + 'broken-signature'; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /Invalid token signature/ + ); + }); - it('should fail due to no issue time', () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ), - /No issue time/ - ); - }); + it('should fail due to no expiration date', () => { + const now = new Date().getTime() / 1000; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /No expiration time/ + ); + }); - it('should fail due to certificate with expiration date in future', () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + 2 * maxLifetimeSecs; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ), - /Expiration time too far in future/ - ); - }); + it('should fail due to no issue time', () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /No issue time/ + ); + }); - it('should pass due to expiration date in future with adjusted max expiry', async () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + 2 * maxLifetimeSecs; - const maxExpiry = 3 * maxLifetimeSecs; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - await client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience', - ['testissuer'], - maxExpiry - ); - }); + it('should fail due to certificate with expiration date in future', () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + 2 * maxLifetimeSecs; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /Expiration time too far in future/ + ); + }); - it('should fail due to token being used to early', () => { - const maxLifetimeSecs = 86400; - const clockSkews = 300; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const issueTime = now + clockSkews * 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - issueTime + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( + it('should pass due to expiration date in future with adjusted max expiry', async () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + 2 * maxLifetimeSecs; + const maxExpiry = 3 * maxLifetimeSecs; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + await client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' - ), - /Token used too early/ - ); - }); + 'testaudience', + ['testissuer'], + maxExpiry + ); + }); - it('should fail due to token being used to late', () => { - const maxLifetimeSecs = 86400; - const clockSkews = 300; - const now = new Date().getTime() / 1000; - const expiry = now - maxLifetimeSecs / 2; - const issueTime = now - clockSkews * 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - issueTime + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience' - ), - /Token used too late/ - ); - }); + it('should fail due to token being used to early', () => { + const maxLifetimeSecs = 86400; + const clockSkews = 300; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const issueTime = now + clockSkews * 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + issueTime + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /Token used too early/ + ); + }); + + it('should fail due to token being used to late', () => { + const maxLifetimeSecs = 86400; + const clockSkews = 300; + const now = new Date().getTime() / 1000; + const expiry = now - maxLifetimeSecs / 2; + const issueTime = now - clockSkews * 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + issueTime + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience' + ), + /Token used too late/ + ); + }); + + it('should fail due to invalid issuer', () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"invalidissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + return assertRejects( + client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKey}, + 'testaudience', + ['testissuer'] + ), + /Invalid issuer/ + ); + }); - it('should fail due to invalid issuer', () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"invalidissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - return assertRejects( - client.verifySignedJwtWithCertsAsync( + it('should pass due to valid issuer', async () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('sha256'); + signer.update(data); + const signature = signer.sign(privateKey, 'base64'); + data += '.' + signature; + await client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, 'testaudience', ['testissuer'] - ), - /Invalid issuer/ - ); - }); - - it('should pass due to valid issuer', async () => { - const maxLifetimeSecs = 86400; - const now = new Date().getTime() / 1000; - const expiry = now + maxLifetimeSecs / 2; - const idToken = - '{' + - '"iss":"testissuer",' + - '"aud":"testaudience",' + - '"azp":"testauthorisedparty",' + - '"email_verified":"true",' + - '"id":"123456789",' + - '"sub":"123456789",' + - '"email":"test@test.com",' + - '"iat":' + - now + - ',' + - '"exp":' + - expiry + - '}'; - const envelope = '{' + '"kid":"keyid",' + '"alg":"RS256"' + '}'; - let data = - Buffer.from(envelope).toString('base64') + - '.' + - Buffer.from(idToken).toString('base64'); - const signer = crypto.createSign('sha256'); - signer.update(data); - const signature = signer.sign(privateKey, 'base64'); - data += '.' + signature; - await client.verifySignedJwtWithCertsAsync( - data, - {keyid: publicKey}, - 'testaudience', - ['testissuer'] - ); - }); - - it('should be able to retrieve a list of Google certificates', done => { - const scope = nock('https://www.googleapis.com') - .get(certsPath) - .replyWithFile(200, certsResPath); - client.getFederatedSignonCerts((err, certs) => { - assert.strictEqual(err, null); - assert.strictEqual(Object.keys(certs!).length, 2); - assert.notStrictEqual( - certs!.a15eea964ab9cce480e5ef4f47cb17b9fa7d0b21, - null - ); - assert.notStrictEqual( - certs!['39596dc3a3f12aa74b481579e4ec944f86d24b95'], - null ); - scope.done(); - done(); }); - }); - it('should be able to retrieve a list of Google certificates from cache again', done => { - const scope = nock('https://www.googleapis.com') - .defaultReplyHeaders({ - 'Cache-Control': 'public, max-age=23641, must-revalidate, no-transform', - 'Content-Type': 'application/json', - }) - .get(certsPath) - .replyWithFile(200, certsResPath); - client.getFederatedSignonCerts((err, certs) => { - assert.strictEqual(err, null); - assert.strictEqual(Object.keys(certs!).length, 2); - scope.done(); // has retrieved from nock... nock no longer will reply - client.getFederatedSignonCerts((err2, certs2) => { - assert.strictEqual(err2, null); - assert.strictEqual(Object.keys(certs2!).length, 2); + it('should be able to retrieve a list of Google certificates', done => { + const scope = nock('https://www.googleapis.com') + .get(certsPath) + .replyWithFile(200, certsResPath); + client.getFederatedSignonCerts((err, certs) => { + assert.strictEqual(err, null); + assert.strictEqual(Object.keys(certs!).length, 2); + assert.notStrictEqual( + certs!.a15eea964ab9cce480e5ef4f47cb17b9fa7d0b21, + null + ); + assert.notStrictEqual( + certs!['39596dc3a3f12aa74b481579e4ec944f86d24b95'], + null + ); scope.done(); done(); }); }); - }); - - it('should set redirect_uri if not provided in options', () => { - const generated = client.generateAuthUrl({}); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.redirect_uri, REDIRECT_URI); - }); - it('should set client_id if not provided in options', () => { - const generated = client.generateAuthUrl({}); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.client_id, CLIENT_ID); - }); + it('should be able to retrieve a list of Google certificates from cache again', done => { + const scope = nock('https://www.googleapis.com') + .defaultReplyHeaders({ + 'Cache-Control': + 'public, max-age=23641, must-revalidate, no-transform', + 'Content-Type': 'application/json', + }) + .get(certsPath) + .replyWithFile(200, certsResPath); + client.getFederatedSignonCerts((err, certs) => { + assert.strictEqual(err, null); + assert.strictEqual(Object.keys(certs!).length, 2); + scope.done(); // has retrieved from nock... nock no longer will reply + client.getFederatedSignonCerts((err2, certs2) => { + assert.strictEqual(err2, null); + assert.strictEqual(Object.keys(certs2!).length, 2); + scope.done(); + done(); + }); + }); + }); - it('should override redirect_uri if provided in options', () => { - const generated = client.generateAuthUrl({redirect_uri: 'overridden'}); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.redirect_uri, 'overridden'); - }); + it('should set redirect_uri if not provided in options', () => { + const generated = client.generateAuthUrl({}); + const parsed = url.parse(generated); + if (typeof parsed.query !== 'string') { + throw new Error('Unable to parse querystring'); + } + const query = qs.parse(parsed.query); + assert.strictEqual(query.redirect_uri, REDIRECT_URI); + }); - it('should override client_id if provided in options', () => { - const generated = client.generateAuthUrl({client_id: 'client_override'}); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.client_id, 'client_override'); - }); + it('should set client_id if not provided in options', () => { + const generated = client.generateAuthUrl({}); + const parsed = url.parse(generated); + if (typeof parsed.query !== 'string') { + throw new Error('Unable to parse querystring'); + } + const query = qs.parse(parsed.query); + assert.strictEqual(query.client_id, CLIENT_ID); + }); - it('should return error in callback on request', done => { - client.request({}, (err, result) => { - assert.strictEqual( - err!.message, - 'No access, refresh token or API key is set.' - ); - assert.strictEqual(result, undefined); - done(); + it('should override redirect_uri if provided in options', () => { + const generated = client.generateAuthUrl({redirect_uri: 'overridden'}); + const parsed = url.parse(generated); + if (typeof parsed.query !== 'string') { + throw new Error('Unable to parse querystring'); + } + const query = qs.parse(parsed.query); + assert.strictEqual(query.redirect_uri, 'overridden'); }); - }); - it('should not emit warning on refreshAccessToken', async () => { - let warned = false; - sandbox.stub(process, 'emitWarning').callsFake(() => (warned = true)); - client.refreshAccessToken((err, result) => { - assert.strictEqual(warned, false); + it('should override client_id if provided in options', () => { + const generated = client.generateAuthUrl({client_id: 'client_override'}); + const parsed = url.parse(generated); + if (typeof parsed.query !== 'string') { + throw new Error('Unable to parse querystring'); + } + const query = qs.parse(parsed.query); + assert.strictEqual(query.client_id, 'client_override'); }); - }); - it('should return error in callback on refreshAccessToken', done => { - client.refreshAccessToken((err, result) => { - assert.strictEqual(err!.message, 'No refresh token is set.'); - assert.strictEqual(result, undefined); - done(); + it('should return error in callback on request', done => { + client.request({}, (err, result) => { + assert.strictEqual( + err!.message, + 'No access, refresh token or API key is set.' + ); + assert.strictEqual(result, undefined); + done(); + }); }); - }); - function mockExample() { - return [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc123', expires_in: 1}), - nock('http://example.com') - .get('/') - .reply(200), - ]; - } - - it('should refresh token if missing access token', done => { - const scopes = mockExample(); - const accessToken = 'abc123'; - let raisedEvent = false; - const refreshToken = 'refresh-token-placeholder'; - client.credentials = {refresh_token: refreshToken}; - - // ensure the tokens event is raised - client.on('tokens', tokens => { - assert.strictEqual(tokens.access_token, accessToken); - raisedEvent = true; + it('should not emit warning on refreshAccessToken', async () => { + let warned = false; + sandbox.stub(process, 'emitWarning').callsFake(() => (warned = true)); + client.refreshAccessToken((err, result) => { + assert.strictEqual(warned, false); + }); }); - client.request({url: 'http://example.com'}, err => { - scopes.forEach(s => s.done()); - assert(raisedEvent); - assert.strictEqual(accessToken, client.credentials.access_token); - done(); + it('should return error in callback on refreshAccessToken', done => { + client.refreshAccessToken((err, result) => { + assert.strictEqual(err!.message, 'No refresh token is set.'); + assert.strictEqual(result, undefined); + done(); + }); }); - }); - it('should unify the promise when refreshing the token', async () => { - // Mock a single call to the token server, and 3 calls to the example - // endpoint. This makes sure that refreshToken is called only once. - const scopes = [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc123', expires_in: 1}), - nock('http://example.com') - .get('/') - .thrice() - .reply(200), - ]; - client.credentials = {refresh_token: 'refresh-token-placeholder'}; - await Promise.all([ - client.request({url: 'http://example.com'}), - client.request({url: 'http://example.com'}), - client.request({url: 'http://example.com'}), - ]); - scopes.forEach(s => s.done()); - assert.strictEqual('abc123', client.credentials.access_token); - }); + function mockExample() { + return [ + nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, {access_token: 'abc123', expires_in: 1}), + nock('http://example.com') + .get('/') + .reply(200), + ]; + } - it('should clear the cached refresh token promise after completion', async () => { - // Mock 2 calls to the token server and 2 calls to the example endpoint. - // This makes sure that the token endpoint is invoked twice, preventing - // the promise from getting cached for too long. - const scopes = [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) - .twice() - .reply(200, {access_token: 'abc123', expires_in: 100000}), - nock('http://example.com') - .get('/') - .twice() - .reply(200), - ]; - client.credentials = {refresh_token: 'refresh-token-placeholder'}; - await client.request({url: 'http://example.com'}); - client.credentials.access_token = null; - await client.request({url: 'http://example.com'}); - scopes.forEach(s => s.done()); - assert.strictEqual('abc123', client.credentials.access_token); - }); + it('should refresh token if missing access token', done => { + const scopes = mockExample(); + const accessToken = 'abc123'; + let raisedEvent = false; + const refreshToken = 'refresh-token-placeholder'; + client.credentials = {refresh_token: refreshToken}; + + // ensure the tokens event is raised + client.on('tokens', tokens => { + assert.strictEqual(tokens.access_token, accessToken); + raisedEvent = true; + }); - it('should clear the cached refresh token promise after throw', async () => { - // Mock a failed call to the refreshToken endpoint. This should trigger - // a second call to refreshToken, which should use a different promise. - const scopes = [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) - .reply(500) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc123', expires_in: 100000}), - nock('http://example.com') - .get('/') - .reply(200), - ]; - client.credentials = {refresh_token: 'refresh-token-placeholder'}; - try { - await client.request({url: 'http://example.com'}); - } catch (e) {} - await client.request({url: 'http://example.com'}); - scopes.forEach(s => s.done()); - assert.strictEqual('abc123', client.credentials.access_token); - }); + client.request({url: 'http://example.com'}, err => { + scopes.forEach(s => s.done()); + assert(raisedEvent); + assert.strictEqual(accessToken, client.credentials.access_token); + done(); + }); + }); - it('should refresh if access token is expired', done => { - client.setCredentials({ - access_token: 'initial-access-token', - refresh_token: 'refresh-token-placeholder', - expiry_date: new Date().getTime() - 1000, + it('should unify the promise when refreshing the token', async () => { + // Mock a single call to the token server, and 3 calls to the example + // endpoint. This makes sure that refreshToken is called only once. + const scopes = [ + nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, {access_token: 'abc123', expires_in: 1}), + nock('http://example.com') + .get('/') + .thrice() + .reply(200), + ]; + client.credentials = {refresh_token: 'refresh-token-placeholder'}; + await Promise.all([ + client.request({url: 'http://example.com'}), + client.request({url: 'http://example.com'}), + client.request({url: 'http://example.com'}), + ]); + scopes.forEach(s => s.done()); + assert.strictEqual('abc123', client.credentials.access_token); }); - const scopes = mockExample(); - client.request({url: 'http://example.com'}, () => { + + it('should clear the cached refresh token promise after completion', async () => { + // Mock 2 calls to the token server and 2 calls to the example endpoint. + // This makes sure that the token endpoint is invoked twice, preventing + // the promise from getting cached for too long. + const scopes = [ + nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .twice() + .reply(200, {access_token: 'abc123', expires_in: 100000}), + nock('http://example.com') + .get('/') + .twice() + .reply(200), + ]; + client.credentials = {refresh_token: 'refresh-token-placeholder'}; + await client.request({url: 'http://example.com'}); + client.credentials.access_token = null; + await client.request({url: 'http://example.com'}); scopes.forEach(s => s.done()); assert.strictEqual('abc123', client.credentials.access_token); - done(); }); - }); - it('should refresh if access token will expired soon and time to refresh before expiration is set', async () => { - const client = new OAuth2Client({ - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - redirectUri: REDIRECT_URI, - eagerRefreshThresholdMillis: 5000, + it('should clear the cached refresh token promise after throw', async () => { + // Mock a failed call to the refreshToken endpoint. This should trigger + // a second call to refreshToken, which should use a different promise. + const scopes = [ + nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .reply(500) + .post('/token', undefined, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, {access_token: 'abc123', expires_in: 100000}), + nock('http://example.com') + .get('/') + .reply(200), + ]; + client.credentials = {refresh_token: 'refresh-token-placeholder'}; + try { + await client.request({url: 'http://example.com'}); + } catch (e) {} + await client.request({url: 'http://example.com'}); + scopes.forEach(s => s.done()); + assert.strictEqual('abc123', client.credentials.access_token); }); - client.credentials = { - access_token: 'initial-access-token', - refresh_token: 'refresh-token-placeholder', - expiry_date: new Date().getTime() + 3000, - }; - const scopes = mockExample(); - await client.request({url: 'http://example.com'}); - assert.strictEqual('abc123', client.credentials.access_token); - scopes.forEach(s => s.done()); - }); - it('should not refresh if access token will not expire soon and time to refresh before expiration is set', async () => { - const client = new OAuth2Client({ - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - redirectUri: REDIRECT_URI, - eagerRefreshThresholdMillis: 5000, + it('should refresh if access token is expired', done => { + client.setCredentials({ + access_token: 'initial-access-token', + refresh_token: 'refresh-token-placeholder', + expiry_date: new Date().getTime() - 1000, + }); + const scopes = mockExample(); + client.request({url: 'http://example.com'}, () => { + scopes.forEach(s => s.done()); + assert.strictEqual('abc123', client.credentials.access_token); + done(); + }); }); - client.credentials = { - access_token: 'initial-access-token', - refresh_token: 'refresh-token-placeholder', - expiry_date: new Date().getTime() + 10000, - }; - const scopes = mockExample(); - await client.request({url: 'http://example.com'}); - assert.strictEqual('initial-access-token', client.credentials.access_token); - assert.strictEqual(false, scopes[0].isDone()); - scopes[1].done(); - }); - it('should not refresh if not expired', done => { - client.credentials = { - access_token: 'initial-access-token', - refresh_token: 'refresh-token-placeholder', - expiry_date: new Date().getTime() + 500000, - }; - const scopes = mockExample(); - client.request({url: 'http://example.com'}, () => { - assert.strictEqual( - 'initial-access-token', - client.credentials.access_token - ); - assert.strictEqual(false, scopes[0].isDone()); - scopes[1].done(); - done(); + it('should refresh if access token will expired soon and time to refresh before expiration is set', async () => { + const client = new OAuth2Client({ + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + redirectUri: REDIRECT_URI, + eagerRefreshThresholdMillis: 5000, + }); + client.credentials = { + access_token: 'initial-access-token', + refresh_token: 'refresh-token-placeholder', + expiry_date: new Date().getTime() + 3000, + }; + const scopes = mockExample(); + await client.request({url: 'http://example.com'}); + assert.strictEqual('abc123', client.credentials.access_token); + scopes.forEach(s => s.done()); }); - }); - it('should assume access token is not expired', done => { - client.credentials = { - access_token: 'initial-access-token', - refresh_token: 'refresh-token-placeholder', - }; - const scopes = mockExample(); - client.request({url: 'http://example.com'}, () => { + it('should not refresh if access token will not expire soon and time to refresh before expiration is set', async () => { + const client = new OAuth2Client({ + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + redirectUri: REDIRECT_URI, + eagerRefreshThresholdMillis: 5000, + }); + client.credentials = { + access_token: 'initial-access-token', + refresh_token: 'refresh-token-placeholder', + expiry_date: new Date().getTime() + 10000, + }; + const scopes = mockExample(); + await client.request({url: 'http://example.com'}); assert.strictEqual( 'initial-access-token', client.credentials.access_token ); assert.strictEqual(false, scopes[0].isDone()); scopes[1].done(); - done(); }); - }); - [401, 403].forEach(code => { - it(`should refresh token if the server returns ${code}`, done => { - const scope = nock('http://example.com') - .get('/access') - .reply(code, { - error: {code, message: 'Invalid Credentials'}, - }); + it('should not refresh if not expired', done => { + client.credentials = { + access_token: 'initial-access-token', + refresh_token: 'refresh-token-placeholder', + expiry_date: new Date().getTime() + 500000, + }; const scopes = mockExample(); + client.request({url: 'http://example.com'}, () => { + assert.strictEqual( + 'initial-access-token', + client.credentials.access_token + ); + assert.strictEqual(false, scopes[0].isDone()); + scopes[1].done(); + done(); + }); + }); + + it('should assume access token is not expired', done => { client.credentials = { access_token: 'initial-access-token', refresh_token: 'refresh-token-placeholder', }; - client.request({url: 'http://example.com/access'}, err => { - scope.done(); - scopes[0].done(); - assert.strictEqual('abc123', client.credentials.access_token); + const scopes = mockExample(); + client.request({url: 'http://example.com'}, () => { + assert.strictEqual( + 'initial-access-token', + client.credentials.access_token + ); + assert.strictEqual(false, scopes[0].isDone()); + scopes[1].done(); done(); }); }); - it(`should refresh token if the server returns ${code} with forceRefreshOnFailure`, done => { - const client = new OAuth2Client({ - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - redirectUri: REDIRECT_URI, - forceRefreshOnFailure: true, + [401, 403].forEach(code => { + it(`should refresh token if the server returns ${code}`, done => { + const scope = nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }); + const scopes = mockExample(); + client.credentials = { + access_token: 'initial-access-token', + refresh_token: 'refresh-token-placeholder', + }; + client.request({url: 'http://example.com/access'}, err => { + scope.done(); + scopes[0].done(); + assert.strictEqual('abc123', client.credentials.access_token); + done(); + }); }); - const scope = nock('http://example.com') - .get('/access') - .reply(code, { - error: {code, message: 'Invalid Credentials'}, + + it(`should refresh token if the server returns ${code} with forceRefreshOnFailure`, done => { + const client = new OAuth2Client({ + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + redirectUri: REDIRECT_URI, + forceRefreshOnFailure: true, }); - const scopes = mockExample(); + const scope = nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }); + const scopes = mockExample(); + client.credentials = { + access_token: 'initial-access-token', + refresh_token: 'refresh-token-placeholder', + expiry_date: new Date().getTime() + 500000, + }; + client.request({url: 'http://example.com/access'}, err => { + scope.done(); + scopes[0].done(); + assert.strictEqual('abc123', client.credentials.access_token); + done(); + }); + }); + }); + + it('should not retry requests with streaming data', done => { + const s = fs.createReadStream('./test/fixtures/public.pem'); + const scope = nock('http://example.com') + .post('/') + .reply(401); client.credentials = { access_token: 'initial-access-token', refresh_token: 'refresh-token-placeholder', expiry_date: new Date().getTime() + 500000, }; - client.request({url: 'http://example.com/access'}, err => { + client.request( + {method: 'POST', url: 'http://example.com', data: s}, + err => { + scope.done(); + const e = err as GaxiosError; + assert(e); + assert.strictEqual(e.response!.status, 401); + done(); + } + ); + }); + + it('should revoke credentials if access token present', done => { + const scope = nock('https://oauth2.googleapis.com') + .post('/revoke?token=abc') + .reply(200, {success: true}); + client.credentials = {access_token: 'abc', refresh_token: 'abc'}; + client.revokeCredentials((err, result) => { + assert.strictEqual(err, null); + assert.strictEqual(result!.data!.success, true); + assert.deepStrictEqual(client.credentials, {}); scope.done(); - scopes[0].done(); - assert.strictEqual('abc123', client.credentials.access_token); done(); }); }); - }); - it('should not retry requests with streaming data', done => { - const s = fs.createReadStream('./test/fixtures/public.pem'); - const scope = nock('http://example.com') - .post('/') - .reply(401); - client.credentials = { - access_token: 'initial-access-token', - refresh_token: 'refresh-token-placeholder', - expiry_date: new Date().getTime() + 500000, - }; - client.request( - {method: 'POST', url: 'http://example.com', data: s}, - err => { - scope.done(); - const e = err as GaxiosError; - assert(e); - assert.strictEqual(e.response!.status, 401); + it('should clear credentials and return error if no access token to revoke', done => { + client.credentials = {refresh_token: 'abc'}; + client.revokeCredentials((err, result) => { + assert.strictEqual(err!.message, 'No access token to revoke.'); + assert.strictEqual(result, undefined); + assert.deepStrictEqual(client.credentials, {}); done(); - } - ); - }); + }); + }); - it('should revoke credentials if access token present', done => { - const scope = nock('https://oauth2.googleapis.com') - .post('/revoke?token=abc') - .reply(200, {success: true}); - client.credentials = {access_token: 'abc', refresh_token: 'abc'}; - client.revokeCredentials((err, result) => { - assert.strictEqual(err, null); - assert.strictEqual(result!.data!.success, true); - assert.deepStrictEqual(client.credentials, {}); + it('getToken should allow a code_verifier to be passed', async () => { + const scope = nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + const res = await client.getToken({ + code: 'code here', + codeVerifier: 'its_verified', + }); scope.done(); - done(); + assert(res.res); + if (!res.res) return; + const params = qs.parse(res.res.config.data); + assert.strictEqual(params.code_verifier, 'its_verified'); }); - }); - it('should clear credentials and return error if no access token to revoke', done => { - client.credentials = {refresh_token: 'abc'}; - client.revokeCredentials((err, result) => { - assert.strictEqual(err!.message, 'No access token to revoke.'); - assert.strictEqual(result, undefined); - assert.deepStrictEqual(client.credentials, {}); - done(); + it('getToken should set redirect_uri if not provided in options', async () => { + const scope = nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + const res = await client.getToken({code: 'code here'}); + scope.done(); + assert(res.res); + if (!res.res) return; + const params = qs.parse(res.res.config.data); + assert.strictEqual(params.redirect_uri, REDIRECT_URI); }); - }); - it('getToken should allow a code_verifier to be passed', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc', refresh_token: '123', expires_in: 10}); - const res = await client.getToken({ - code: 'code here', - codeVerifier: 'its_verified', + it('getToken should set client_id if not provided in options', async () => { + const scope = nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + const res = await client.getToken({code: 'code here'}); + scope.done(); + assert(res.res); + if (!res.res) return; + const params = qs.parse(res.res.config.data); + assert.strictEqual(params.client_id, CLIENT_ID); }); - scope.done(); - assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.code_verifier, 'its_verified'); - }); - - it('getToken should set redirect_uri if not provided in options', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc', refresh_token: '123', expires_in: 10}); - const res = await client.getToken({code: 'code here'}); - scope.done(); - assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.redirect_uri, REDIRECT_URI); - }); - it('getToken should set client_id if not provided in options', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc', refresh_token: '123', expires_in: 10}); - const res = await client.getToken({code: 'code here'}); - scope.done(); - assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.client_id, CLIENT_ID); - }); + it('getToken should override redirect_uri if provided in options', async () => { + const scope = nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + const res = await client.getToken({ + code: 'code here', + redirect_uri: 'overridden', + }); + scope.done(); + assert(res.res); + if (!res.res) return; + const params = qs.parse(res.res.config.data); + assert.strictEqual(params.redirect_uri, 'overridden'); + }); - it('getToken should override redirect_uri if provided in options', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc', refresh_token: '123', expires_in: 10}); - const res = await client.getToken({ - code: 'code here', - redirect_uri: 'overridden', + it('getToken should override client_id if provided in options', async () => { + const scope = nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + const res = await client.getToken({ + code: 'code here', + client_id: 'overridden', + }); + scope.done(); + assert(res.res); + if (!res.res) return; + const params = qs.parse(res.res.config.data); + assert.strictEqual(params.client_id, 'overridden'); }); - scope.done(); - assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.redirect_uri, 'overridden'); - }); - it('getToken should override client_id if provided in options', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc', refresh_token: '123', expires_in: 10}); - const res = await client.getToken({ - code: 'code here', - client_id: 'overridden', + it('should return expiry_date', done => { + const now = new Date().getTime(); + const scope = nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + client.getToken('code here', (err, tokens) => { + assert(tokens!.expiry_date! >= now + 10 * 1000); + assert(tokens!.expiry_date! <= now + 15 * 1000); + scope.done(); + done(); + }); }); - scope.done(); - assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.client_id, 'overridden'); - }); - it('should return expiry_date', done => { - const now = new Date().getTime(); - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) - .reply(200, {access_token: 'abc', refresh_token: '123', expires_in: 10}); - client.getToken('code here', (err, tokens) => { - assert(tokens!.expiry_date! >= now + 10 * 1000); - assert(tokens!.expiry_date! <= now + 15 * 1000); + it('should obtain token info', async () => { + const accessToken = 'abc'; + const tokenInfo = { + aud: 'naudience', + user_id: '12345', + scope: 'scope1 scope2', + expires_in: 1234, + }; + + const scope = nock(baseUrl) + .get(`/tokeninfo?access_token=${accessToken}`) + .reply(200, tokenInfo); + + const info = await client.getTokenInfo(accessToken); scope.done(); - done(); + assert.strictEqual(info.aud, tokenInfo.aud); + assert.strictEqual(info.user_id, tokenInfo.user_id); + assert.deepStrictEqual(info.scopes, tokenInfo.scope.split(' ')); }); - }); - - it('should obtain token info', async () => { - const accessToken = 'abc'; - const tokenInfo = { - aud: 'naudience', - user_id: '12345', - scope: 'scope1 scope2', - expires_in: 1234, - }; - - const scope = nock(baseUrl) - .get(`/tokeninfo?access_token=${accessToken}`) - .reply(200, tokenInfo); - - const info = await client.getTokenInfo(accessToken); - scope.done(); - assert.strictEqual(info.aud, tokenInfo.aud); - assert.strictEqual(info.user_id, tokenInfo.user_id); - assert.deepStrictEqual(info.scopes, tokenInfo.scope.split(' ')); - }); - it('should warn about deprecation of getRequestMetadata', done => { - const stub = sandbox.stub(messages, 'warn'); - client.getRequestMetadata(null, () => { - assert.strictEqual(stub.calledOnce, true); - done(); + it('should warn about deprecation of getRequestMetadata', done => { + const stub = sandbox.stub(messages, 'warn'); + client.getRequestMetadata(null, () => { + assert.strictEqual(stub.calledOnce, true); + done(); + }); }); - }); - it('should throw if tries to refresh but no refresh token is available', async () => { - client.setCredentials({ - access_token: 'initial-access-token', - expiry_date: new Date().getTime() - 1000, + it('should throw if tries to refresh but no refresh token is available', async () => { + client.setCredentials({ + access_token: 'initial-access-token', + expiry_date: new Date().getTime() - 1000, + }); + await assertRejects( + client.getRequestHeaders('http://example.com'), + /No refresh token is set./ + ); }); - await assertRejects( - client.getRequestHeaders('http://example.com'), - /No refresh token is set./ - ); }); }); diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 67e10b94..847f59b7 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -18,133 +18,138 @@ import * as fs from 'fs'; import * as nock from 'nock'; import {UserRefreshClient} from '../src'; -// Creates a standard JSON credentials object for testing. -function createJSON() { - return { - client_secret: 'privatekey', - client_id: 'client123', - refresh_token: 'refreshtoken', - type: 'authorized_user', - }; -} - -it('populates credentials.refresh_token if provided', () => { - const refresh = new UserRefreshClient({ - refreshToken: 'abc123', +describe('refresh', () => { + // Creates a standard JSON credentials object for testing. + function createJSON() { + return { + client_secret: 'privatekey', + client_id: 'client123', + refresh_token: 'refreshtoken', + type: 'authorized_user', + }; + } + + it('populates credentials.refresh_token if provided', () => { + const refresh = new UserRefreshClient({ + refreshToken: 'abc123', + }); + assert.strictEqual(refresh.credentials.refresh_token, 'abc123'); }); - assert.strictEqual(refresh.credentials.refresh_token, 'abc123'); -}); -it('fromJSON should error on null json', () => { - const refresh = new UserRefreshClient(); - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (refresh as any).fromJSON(null); + it('fromJSON should error on null json', () => { + const refresh = new UserRefreshClient(); + assert.throws(() => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (refresh as any).fromJSON(null); + }); }); -}); -it('fromJSON should error on empty json', () => { - const refresh = new UserRefreshClient(); - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - refresh.fromJSON({}); + it('fromJSON should error on empty json', () => { + const refresh = new UserRefreshClient(); + assert.throws(() => { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + refresh.fromJSON({}); + }); }); -}); -it('fromJSON should error on missing client_id', () => { - const json = createJSON(); - delete json.client_id; - const refresh = new UserRefreshClient(); - assert.throws(() => { - refresh.fromJSON(json); + it('fromJSON should error on missing client_id', () => { + const json = createJSON(); + delete json.client_id; + const refresh = new UserRefreshClient(); + assert.throws(() => { + refresh.fromJSON(json); + }); }); -}); -it('fromJSON should error on missing client_secret', () => { - const json = createJSON(); - delete json.client_secret; - const refresh = new UserRefreshClient(); - assert.throws(() => { - refresh.fromJSON(json); + it('fromJSON should error on missing client_secret', () => { + const json = createJSON(); + delete json.client_secret; + const refresh = new UserRefreshClient(); + assert.throws(() => { + refresh.fromJSON(json); + }); }); -}); -it('fromJSON should error on missing refresh_token', () => { - const json = createJSON(); - delete json.refresh_token; - const refresh = new UserRefreshClient(); - assert.throws(() => { - refresh.fromJSON(json); + it('fromJSON should error on missing refresh_token', () => { + const json = createJSON(); + delete json.refresh_token; + const refresh = new UserRefreshClient(); + assert.throws(() => { + refresh.fromJSON(json); + }); }); -}); - -it('fromJSON should create UserRefreshClient with clientId_', () => { - const json = createJSON(); - const refresh = new UserRefreshClient(); - const result = refresh.fromJSON(json); - assert.strictEqual(json.client_id, refresh._clientId); -}); -it('fromJSON should create UserRefreshClient with clientSecret_', () => { - const json = createJSON(); - const refresh = new UserRefreshClient(); - const result = refresh.fromJSON(json); - assert.strictEqual(json.client_secret, refresh._clientSecret); -}); - -it('fromJSON should create UserRefreshClient with _refreshToken', () => { - const json = createJSON(); - const refresh = new UserRefreshClient(); - const result = refresh.fromJSON(json); - assert.strictEqual(json.refresh_token, refresh._refreshToken); -}); + it('fromJSON should create UserRefreshClient with clientId_', () => { + const json = createJSON(); + const refresh = new UserRefreshClient(); + const result = refresh.fromJSON(json); + assert.strictEqual(json.client_id, refresh._clientId); + }); -it('fromStream should error on null stream', done => { - const refresh = new UserRefreshClient(); - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (refresh as any).fromStream(null, (err: Error) => { - assert.strictEqual(true, err instanceof Error); - done(); + it('fromJSON should create UserRefreshClient with clientSecret_', () => { + const json = createJSON(); + const refresh = new UserRefreshClient(); + const result = refresh.fromJSON(json); + assert.strictEqual(json.client_secret, refresh._clientSecret); }); -}); -it('fromStream should read the stream and create a UserRefreshClient', done => { - // Read the contents of the file into a json object. - const fileContents = fs.readFileSync('./test/fixtures/refresh.json', 'utf-8'); - const json = JSON.parse(fileContents); + it('fromJSON should create UserRefreshClient with _refreshToken', () => { + const json = createJSON(); + const refresh = new UserRefreshClient(); + const result = refresh.fromJSON(json); + assert.strictEqual(json.refresh_token, refresh._refreshToken); + }); - // Now open a stream on the same file. - const stream = fs.createReadStream('./test/fixtures/refresh.json'); + it('fromStream should error on null stream', done => { + const refresh = new UserRefreshClient(); + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (refresh as any).fromStream(null, (err: Error) => { + assert.strictEqual(true, err instanceof Error); + done(); + }); + }); - // And pass it into the fromStream method. - const refresh = new UserRefreshClient(); - refresh.fromStream(stream, err => { - assert.ifError(err); - // Ensure that the correct bits were pulled from the stream. - assert.strictEqual(json.client_id, refresh._clientId); - assert.strictEqual(json.client_secret, refresh._clientSecret); - assert.strictEqual(json.refresh_token, refresh._refreshToken); - done(); + it('fromStream should read the stream and create a UserRefreshClient', done => { + // Read the contents of the file into a json object. + const fileContents = fs.readFileSync( + './test/fixtures/refresh.json', + 'utf-8' + ); + const json = JSON.parse(fileContents); + + // Now open a stream on the same file. + const stream = fs.createReadStream('./test/fixtures/refresh.json'); + + // And pass it into the fromStream method. + const refresh = new UserRefreshClient(); + refresh.fromStream(stream, err => { + assert.ifError(err); + // Ensure that the correct bits were pulled from the stream. + assert.strictEqual(json.client_id, refresh._clientId); + assert.strictEqual(json.client_secret, refresh._clientSecret); + assert.strictEqual(json.refresh_token, refresh._refreshToken); + done(); + }); }); -}); -it('getRequestHeaders should populate x-goog-user-project header if quota_project_id present', async () => { - // The first time auth.getRequestHeaders() is called /token endpoint is used to - // fetch a JWT. - const req = nock('https://oauth2.googleapis.com') - .post('/token') - .reply(200, {}); - - // Fake loading default credentials with quota project set: - const stream = fs.createReadStream( - './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' - ); - const refresh = new UserRefreshClient(); - await refresh.fromStream(stream); - - const headers = await refresh.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + it('getRequestHeaders should populate x-goog-user-project header if quota_project_id present', async () => { + // The first time auth.getRequestHeaders() is called /token endpoint is used to + // fetch a JWT. + const req = nock('https://oauth2.googleapis.com') + .post('/token') + .reply(200, {}); + + // Fake loading default credentials with quota project set: + const stream = fs.createReadStream( + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + ); + const refresh = new UserRefreshClient(); + await refresh.fromStream(stream); + + const headers = await refresh.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + }); }); diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 3d8ff52e..59a3c7e3 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -13,186 +13,188 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {describe, it, afterEach} from 'mocha'; import {GaxiosOptions} from 'gaxios'; const assertRejects = require('assert-rejects'); import * as nock from 'nock'; import {DefaultTransporter, RequestError} from '../src/transporters'; -const savedEnv = process.env; -afterEach(() => { - process.env = savedEnv; -}); - -nock.disableNetConnect(); +describe('transporters', () => { + const savedEnv = process.env; + afterEach(() => { + process.env = savedEnv; + }); -const defaultUserAgentRE = 'google-api-nodejs-client/\\d+.\\d+.\\d+'; -const transporter = new DefaultTransporter(); + nock.disableNetConnect(); -it('should set default adapter to node.js', () => { - const opts = transporter.configure(); - const re = new RegExp(defaultUserAgentRE); - assert(re.test(opts.headers!['User-Agent'])); -}); + const defaultUserAgentRE = 'google-api-nodejs-client/\\d+.\\d+.\\d+'; + const transporter = new DefaultTransporter(); -it('should append default client user agent to the existing user agent', () => { - const applicationName = 'MyTestApplication-1.0'; - const opts = transporter.configure({ - headers: {'User-Agent': applicationName}, - url: '', + it('should set default adapter to node.js', () => { + const opts = transporter.configure(); + const re = new RegExp(defaultUserAgentRE); + assert(re.test(opts.headers!['User-Agent'])); }); - const re = new RegExp(applicationName + ' ' + defaultUserAgentRE); - assert(re.test(opts.headers!['User-Agent'])); -}); -it('should not append default client user agent to the existing user agent more than once', () => { - const appName = 'MyTestApplication-1.0 google-api-nodejs-client/foobear'; - const opts = transporter.configure({ - headers: {'User-Agent': appName}, - url: '', + it('should append default client user agent to the existing user agent', () => { + const applicationName = 'MyTestApplication-1.0'; + const opts = transporter.configure({ + headers: {'User-Agent': applicationName}, + url: '', + }); + const re = new RegExp(applicationName + ' ' + defaultUserAgentRE); + assert(re.test(opts.headers!['User-Agent'])); }); - assert.strictEqual(opts.headers!['User-Agent'], appName); -}); -it('should add x-goog-api-client header if none exists', () => { - const opts = transporter.configure({ - url: '', + it('should not append default client user agent to the existing user agent more than once', () => { + const appName = 'MyTestApplication-1.0 google-api-nodejs-client/foobear'; + const opts = transporter.configure({ + headers: {'User-Agent': appName}, + url: '', + }); + assert.strictEqual(opts.headers!['User-Agent'], appName); }); - assert( - /^gl-node\/[.-\w$]+ auth\/[.-\w$]+$/.test( - opts.headers!['x-goog-api-client'] - ) - ); -}); -it('should append to x-goog-api-client header if it exists', () => { - const opts = transporter.configure({ - headers: {'x-goog-api-client': 'gdcl/1.0.0'}, - url: '', + it('should add x-goog-api-client header if none exists', () => { + const opts = transporter.configure({ + url: '', + }); + assert( + /^gl-node\/[.-\w$]+ auth\/[.-\w$]+$/.test( + opts.headers!['x-goog-api-client'] + ) + ); }); - assert( - /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client']) - ); -}); -// see: https://github.com/googleapis/google-auth-library-nodejs/issues/819 -it('should not append x-goog-api-client header multiple times', () => { - const opts = { - headers: {'x-goog-api-client': 'gdcl/1.0.0'}, - url: '', - }; - let configuredOpts = transporter.configure(opts); - console.info(configuredOpts); - configuredOpts = transporter.configure(opts); - console.info(configuredOpts); - assert( - /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test( - configuredOpts.headers!['x-goog-api-client'] - ) - ); -}); + it('should append to x-goog-api-client header if it exists', () => { + const opts = transporter.configure({ + headers: {'x-goog-api-client': 'gdcl/1.0.0'}, + url: '', + }); + assert( + /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client']) + ); + }); -it('should create a single error from multiple response errors', done => { - const firstError = {message: 'Error 1'}; - const secondError = {message: 'Error 2'}; - const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(400, {error: {code: 500, errors: [firstError, secondError]}}); - transporter.request({url}, error => { - scope.done(); - assert.strictEqual(error!.message, 'Error 1\nError 2'); - assert.strictEqual((error as RequestError).code, 500); - assert.strictEqual((error as RequestError).errors.length, 2); - done(); + // see: https://github.com/googleapis/google-auth-library-nodejs/issues/819 + it('should not append x-goog-api-client header multiple times', () => { + const opts = { + headers: {'x-goog-api-client': 'gdcl/1.0.0'}, + url: '', + }; + let configuredOpts = transporter.configure(opts); + console.info(configuredOpts); + configuredOpts = transporter.configure(opts); + console.info(configuredOpts); + assert( + /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test( + configuredOpts.headers!['x-goog-api-client'] + ) + ); }); -}); -it('should return an error for a 404 response', done => { - const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(404, 'Not found'); - transporter.request({url}, error => { - scope.done(); - assert.strictEqual(error!.message, 'Not found'); - assert.strictEqual((error as RequestError).code, '404'); - done(); + it('should create a single error from multiple response errors', done => { + const firstError = {message: 'Error 1'}; + const secondError = {message: 'Error 2'}; + const url = 'http://example.com'; + const scope = nock(url) + .get('/') + .reply(400, {error: {code: 500, errors: [firstError, secondError]}}); + transporter.request({url}, error => { + scope.done(); + assert.strictEqual(error!.message, 'Error 1\nError 2'); + assert.strictEqual((error as RequestError).code, 500); + assert.strictEqual((error as RequestError).errors.length, 2); + done(); + }); }); -}); -it('should return an error if you try to use request config options', done => { - const expected = - "'uri' is not a valid configuration option. Please use 'url' instead. This library is using Axios for requests. Please see https://github.com/axios/axios to learn more about the valid request options."; - transporter.request( - { - uri: 'http://example.com/api', - } as GaxiosOptions, - error => { - assert.strictEqual(error!.message, expected); + it('should return an error for a 404 response', done => { + const url = 'http://example.com'; + const scope = nock(url) + .get('/') + .reply(404, 'Not found'); + transporter.request({url}, error => { + scope.done(); + assert.strictEqual(error!.message, 'Not found'); + assert.strictEqual((error as RequestError).code, '404'); done(); - } - ); -}); + }); + }); -it('should return an error if you try to use request config options with a promise', async () => { - const expected = new RegExp( - `'uri' is not a valid configuration option. Please use 'url' instead. This ` + - `library is using Axios for requests. Please see https://github.com/axios/axios ` + - `to learn more about the valid request options.` - ); - const uri = 'http://example.com/api'; - assert.throws(() => transporter.request({uri} as GaxiosOptions), expected); -}); + it('should return an error if you try to use request config options', done => { + const expected = + "'uri' is not a valid configuration option. Please use 'url' instead. This library is using Axios for requests. Please see https://github.com/axios/axios to learn more about the valid request options."; + transporter.request( + { + uri: 'http://example.com/api', + } as GaxiosOptions, + error => { + assert.strictEqual(error!.message, expected); + done(); + } + ); + }); -it('should support invocation with async/await', async () => { - const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(200); - const res = await transporter.request({url}); - scope.done(); - assert.strictEqual(res.status, 200); -}); + it('should return an error if you try to use request config options with a promise', async () => { + const expected = new RegExp( + `'uri' is not a valid configuration option. Please use 'url' instead. This ` + + `library is using Axios for requests. Please see https://github.com/axios/axios ` + + `to learn more about the valid request options.` + ); + const uri = 'http://example.com/api'; + assert.throws(() => transporter.request({uri} as GaxiosOptions), expected); + }); -it('should throw if using async/await', async () => { - const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(500, '🦃'); - await assertRejects(transporter.request({url}), /🦃/); - scope.done(); -}); + it('should support invocation with async/await', async () => { + const url = 'http://example.com'; + const scope = nock(url) + .get('/') + .reply(200); + const res = await transporter.request({url}); + scope.done(); + assert.strictEqual(res.status, 200); + }); -it('should work with a callback', done => { - const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(200); - transporter.request({url}, (err, res) => { + it('should throw if using async/await', async () => { + const url = 'http://example.com'; + const scope = nock(url) + .get('/') + .reply(500, '🦃'); + await assertRejects(transporter.request({url}), /🦃/); scope.done(); - assert.strictEqual(err, null); - assert.strictEqual(res!.status, 200); - done(); }); -}); -// tslint:disable-next-line ban -it.skip('should use the https proxy if one is configured', async () => { - process.env['https_proxy'] = 'https://han:solo@proxy-server:1234'; - const transporter = new DefaultTransporter(); - const scope = nock('https://proxy-server:1234') - .get('https://example.com/fake', undefined, { - reqheaders: { - host: 'example.com', - accept: /.*/g, - 'user-agent': /google-api-nodejs-client\/.*/g, - 'proxy-authorization': /.*/g, - }, - }) - .reply(200); - const url = 'https://example.com/fake'; - const result = await transporter.request({url}); - scope.done(); - assert.strictEqual(result.status, 200); + it('should work with a callback', done => { + const url = 'http://example.com'; + const scope = nock(url) + .get('/') + .reply(200); + transporter.request({url}, (err, res) => { + scope.done(); + assert.strictEqual(err, null); + assert.strictEqual(res!.status, 200); + done(); + }); + }); + + // tslint:disable-next-line ban + it.skip('should use the https proxy if one is configured', async () => { + process.env['https_proxy'] = 'https://han:solo@proxy-server:1234'; + const transporter = new DefaultTransporter(); + const scope = nock('https://proxy-server:1234') + .get('https://example.com/fake', undefined, { + reqheaders: { + host: 'example.com', + accept: /.*/g, + 'user-agent': /google-api-nodejs-client\/.*/g, + 'proxy-authorization': /.*/g, + }, + }) + .reply(200); + const url = 'https://example.com/fake'; + const result = await transporter.request({url}); + scope.done(); + assert.strictEqual(result.status, 200); + }); }); From e671629835c538f62b8285a62a790d8e54ca1d40 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 12 Feb 2020 10:53:57 -0800 Subject: [PATCH 088/662] build: add GitHub actions config for unit tests (#888) --- .github/workflows/ci.yaml | 57 +++++++++++++++++++++++++++++++++++++++ package.json | 3 ++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..4d36c57b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,57 @@ +on: + push: + branches: + - master + pull_request: +name: ci +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node: [8, 10, 12, 13] + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - run: node --version + - run: npm install + - run: npm test + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + - run: npm install + - run: npm test + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 12 + - run: npm install + - run: npm run lint + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 12 + - run: npm install + - run: npm run docs-test + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 12 + - run: npm install + - run: npm test + - run: ./node_modules/.bin/c8 report --reporter=text-lcov | npx codecov@3 -t ${{ secrets.CODECOV_TOKEN }} --pipe diff --git a/package.json b/package.json index 02d9e7ca..6dd5fe5e 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,8 @@ "webpack": "webpack", "browser-test": "karma start", "docs-test": "linkinator docs", - "predocs-test": "npm run docs" + "predocs-test": "npm run docs", + "prelint": "cd samples; npm link ../; npm i" }, "license": "Apache-2.0" } From a98e38678dc4a5e963356378c75c658e36dccd01 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 12 Feb 2020 11:30:58 -0800 Subject: [PATCH 089/662] feat: support for verifying ES256 and retrieving IAP public keys (#887) --- README.md | 32 ++++++++++++- package.json | 1 + samples/verifyIdToken-iap.js | 62 ++++++++++++++++++++++++ src/auth/oauth2client.ts | 71 ++++++++++++++++++++++++++-- test/fixtures/ecdsapublickeys.json | 4 ++ test/fixtures/fake-ecdsa-private.pem | 18 +++++++ test/fixtures/fake-ecdsa-public.pem | 9 ++++ test/test.jwt.ts | 10 ++-- test/test.jwtaccess.ts | 2 +- test/test.oauth2.ts | 66 ++++++++++++++++++++++++++ 10 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 samples/verifyIdToken-iap.js create mode 100644 test/fixtures/ecdsapublickeys.json create mode 100644 test/fixtures/fake-ecdsa-private.pem create mode 100644 test/fixtures/fake-ecdsa-public.pem diff --git a/README.md b/README.md index 7a5324f8..6de836b3 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,7 @@ main().catch(console.error); ``` ## Working with ID Tokens +### Fetching ID Tokens If your application is running behind Cloud Run, or using Cloud Identity-Aware Proxy (IAP), you will need to fetch an ID token to access your application. For this, use the method `getIdTokenClient` on the `GoogleAuth` client. @@ -358,6 +359,8 @@ async function main() { main().catch(console.error); ``` +A complete example can be found in [`samples/idtokens-cloudrun.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-cloudrun.js). + For invoking Cloud Identity-Aware Proxy, you will need to pass the Client ID used when you set up your protected resource as the target audience. @@ -377,7 +380,34 @@ async function main() main().catch(console.error); ``` -See how to [secure your IAP app with signed headers](https://cloud.google.com/iap/docs/signed-headers-howto). +A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). + +### Verifying ID Tokens + +If you've [secured your IAP app with signed headers](https://cloud.google.com/iap/docs/signed-headers-howto), +you can use this library to verify the IAP header: + +```js +const {OAuth2Client} = require('google-auth-library'); +// Expected audience for App Engine. +const expectedAudience = `/projects/your-project-number/apps/your-project-id`; +// IAP issuer +const issuers = ['https://cloud.google.com/iap']; +// Verify the token. OAuth2Client throws an Error if verification fails +const oAuth2Client = new OAuth2Client(); +const response = await oAuth2Client.getIapCerts(); +const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( + idToken, + response.pubkeys, + expectedAudience, + issuers +); + +// Print out the info contained in the IAP ID token +console.log(ticket) +``` + +A complete example can be found in `samples/verifyIdToken-iap.js`. ## Questions/problems? diff --git a/package.json b/package.json index 6dd5fe5e..7699d8b9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", "gcp-metadata": "^3.3.0", diff --git a/samples/verifyIdToken-iap.js b/samples/verifyIdToken-iap.js new file mode 100644 index 00000000..20327534 --- /dev/null +++ b/samples/verifyIdToken-iap.js @@ -0,0 +1,62 @@ +// Copyright 2020 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. + +'use strict'; + +// [START iap_validate_jwt] +const {OAuth2Client} = require('google-auth-library'); + +/** + * Verify the ID token from IAP + * @see https://cloud.google.com/iap/docs/signed-headers-howto + */ +async function main( + iapJwt, + projectNumber = '', + projectId = '', + backendServiceId = '' +) { + // set Audience + let expectedAudience = null; + if (projectNumber && projectId) { + // Expected Audience for App Engine. + expectedAudience = `/projects/${projectNumber}/apps/${projectId}`; + } else if (projectNumber && backendServiceId) { + // Expected Audience for Compute Engine + expectedAudience = `/projects/${projectNumber}/global/backendServices/${backendServiceId}`; + } + + const oAuth2Client = new OAuth2Client(); + + // Verify the id_token, and access the claims. + const response = await oAuth2Client.getIapPublicKeys(); + const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( + iapJwt, + response.pubkeys, + expectedAudience, + ['https://cloud.google.com/iap'] + ); + + // Print out the info contained in the IAP ID token + console.log(ticket); + + if (!expectedAudience) { + console.log( + 'Audience not verified! Supply a projectNumber and projectID to verify' + ); + } +} +// [END iap_validate_jwt] + +const args = process.argv.slice(2); +main(...args).catch(console.error); diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 5dae743f..44637972 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -20,6 +20,7 @@ import { } from 'gaxios'; import * as querystring from 'querystring'; import * as stream from 'stream'; +import * as formatEcdsa from 'ecdsa-sig-formatter'; import {createCrypto, JwkCertificate, hasBrowserCrypto} from '../crypto/crypto'; import * as messages from '../messages'; @@ -28,7 +29,6 @@ import {BodyResponseCallback} from '../transporters'; import {AuthClient} from './authclient'; import {CredentialRequest, Credentials, JWTInput} from './credentials'; import {LoginTicket, TokenPayload} from './loginticket'; - /** * The results from the `generateCodeVerifierAsync` method. To learn more, * See the sample: @@ -51,6 +51,10 @@ export interface Certificates { [index: string]: string | JwkCertificate; } +export interface PublicKeys { + [index: string]: string; +} + export interface Headers { [index: string]: string; } @@ -349,6 +353,19 @@ export interface FederatedSignonCertsResponse { res?: GaxiosResponse | null; } +export interface GetIapPublicKeysCallback { + ( + err: GaxiosError | null, + pubkeys?: PublicKeys, + response?: GaxiosResponse | null + ): void; +} + +export interface IapPublicKeysResponse { + pubkeys: PublicKeys; + res?: GaxiosResponse | null; +} + export interface RevokeCredentialsResult { success: boolean; } @@ -462,6 +479,12 @@ export class OAuth2Client extends AuthClient { private static readonly GOOGLE_OAUTH2_FEDERATED_SIGNON_JWK_CERTS_URL_ = 'https://www.googleapis.com/oauth2/v3/certs'; + /** + * Google Sign on certificates in JWK format. + */ + private static readonly GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_ = + 'https://www.gstatic.com/iap/verify/public_key'; + /** * Clock skew - five minutes in seconds */ @@ -1108,6 +1131,43 @@ export class OAuth2Client extends AuthClient { return {certs: certificates, format, res}; } + /** + * Gets federated sign-on certificates to use for verifying identity tokens. + * Returns certs as array structure, where keys are key ids, and values + * are certificates in either PEM or JWK format. + * @param callback Callback supplying the certificates + */ + getIapPublicKeys(): Promise; + getIapPublicKeys(callback: GetIapPublicKeysCallback): void; + getIapPublicKeys( + callback?: GetIapPublicKeysCallback + ): Promise | void { + if (callback) { + this.getIapPublicKeysAsync().then( + r => callback(null, r.pubkeys, r.res), + callback + ); + } else { + return this.getIapPublicKeysAsync(); + } + } + + async getIapPublicKeysAsync(): Promise { + const nowTime = new Date().getTime(); + + let res: GaxiosResponse; + const url: string = OAuth2Client.GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_; + + try { + res = await this.transporter.request({url}); + } catch (e) { + e.message = `Failed to retrieve verification certificates: ${e.message}`; + throw e; + } + + return {pubkeys: res.data, res}; + } + verifySignedJwtWithCerts() { // To make the code compatible with browser SubtleCrypto we need to make // this method async. @@ -1128,7 +1188,7 @@ export class OAuth2Client extends AuthClient { */ async verifySignedJwtWithCertsAsync( jwt: string, - certs: Certificates, + certs: Certificates | PublicKeys, requiredAudience: string | string[], issuers?: string[], maxExpiry?: number @@ -1144,7 +1204,7 @@ export class OAuth2Client extends AuthClient { throw new Error('Wrong number of segments in token: ' + jwt); } const signed = segments[0] + '.' + segments[1]; - const signature = segments[2]; + let signature = segments[2]; let envelope; let payload: TokenPayload; @@ -1177,6 +1237,11 @@ export class OAuth2Client extends AuthClient { } const cert = certs[envelope.kid]; + + if (envelope.alg === 'ES256') { + signature = formatEcdsa.joseToDer(signature, 'ES256').toString('base64'); + } + const verified = await crypto.verify(cert, signed, signature); if (!verified) { diff --git a/test/fixtures/ecdsapublickeys.json b/test/fixtures/ecdsapublickeys.json new file mode 100644 index 00000000..9bce6a7c --- /dev/null +++ b/test/fixtures/ecdsapublickeys.json @@ -0,0 +1,4 @@ +{ +"2nMJtw": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9e1x7YRZg53A5zIJ0p2ZQ9yTrgPL\nGIf4ntOk+4O2R2+ryIObueyenPXE92tYG1NlKjDNyJLc7tsxi0UUnyxpig==\n-----END PUBLIC KEY-----\n", +"f9R3yg": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESqCmEwytkqG6tL6a2GTQGmSNI4jH\nYo5MeDUs7DpETVhCXXLIFrLg2sZvNqw8SGnnonLoeqgOSqRdjJBGt4I6jQ==\n-----END PUBLIC KEY-----\n" +} \ No newline at end of file diff --git a/test/fixtures/fake-ecdsa-private.pem b/test/fixtures/fake-ecdsa-private.pem new file mode 100644 index 00000000..c0cdbb7e --- /dev/null +++ b/test/fixtures/fake-ecdsa-private.pem @@ -0,0 +1,18 @@ +-----BEGIN EC PARAMETERS----- +MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP////////// +/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6 +k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+ +kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK +fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz +ucrC/GMlUQIBAQ== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIIBaAIBAQQgI59iRg8vYTW9VySmCM3Tvn+WPLKQr132U5HGec3HTgWggfowgfcC +AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// +MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr +vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE +axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W +K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 +YyVRAgEBoUQDQgAEUF5XFvBU1J2PP2Ggh/DiLNv9l4MTM/edN145vGOZIvWe4QBp +FaqzLN7WjTP7BiJCXI044iqRbuDGc2goPf8LMw== +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/fake-ecdsa-public.pem b/test/fixtures/fake-ecdsa-public.pem new file mode 100644 index 00000000..2b1c483b --- /dev/null +++ b/test/fixtures/fake-ecdsa-public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA +AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA//// +///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd +NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5 +RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA +//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABFBeVxbwVNSdjz9hoIfw4izb +/ZeDEzP3nTdeObxjmSL1nuEAaRWqsyze1o0z+wYiQlyNOOIqkW7gxnNoKD3/CzM= +-----END PUBLIC KEY----- diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 339e1406..3cfb3f5f 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -195,7 +195,7 @@ describe('jwt', () => { }); it('gets a jwt header access token', async () => { - const keys = keypair(1024 /* bitsize of private key */); + const keys = keypair(512 /* bitsize of private key */); const email = 'foo@serviceaccount.com'; const jwt = new JWT({ email: 'foo@serviceaccount.com', @@ -216,7 +216,7 @@ describe('jwt', () => { }); it('gets a jwt header access token with key id', async () => { - const keys = keypair(1024 /* bitsize of private key */); + const keys = keypair(512 /* bitsize of private key */); const jwt = new JWT({ email: 'foo@serviceaccount.com', key: keys.private, @@ -236,7 +236,7 @@ describe('jwt', () => { }); it('should accept additionalClaims', async () => { - const keys = keypair(1024 /* bitsize of private key */); + const keys = keypair(512 /* bitsize of private key */); const someClaim = 'cat-on-my-desk'; const jwt = new JWT({ email: 'foo@serviceaccount.com', @@ -256,7 +256,7 @@ describe('jwt', () => { }); it('should accept additionalClaims that include a target_audience', async () => { - const keys = keypair(1024 /* bitsize of private key */); + const keys = keypair(512 /* bitsize of private key */); const jwt = new JWT({ email: 'foo@serviceaccount.com', key: keys.private, @@ -821,7 +821,7 @@ describe('jwt', () => { credentials: Object.assign( require('../../test/fixtures/service-account-with-quota.json'), { - private_key: keypair(1024 /* bitsize of private key */).private, + private_key: keypair(512 /* bitsize of private key */).private, } ), }); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index c074eeed..b76066cc 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -33,7 +33,7 @@ describe('jwtaccess', () => { type: 'service_account', }; - const keys = keypair(1024 /* bitsize of private key */); + const keys = keypair(512 /* bitsize of private key */); const testUri = 'http:/example.com/my_test_service'; const email = 'foo@serviceaccount.com'; diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 850594e8..2ddc95c5 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -16,6 +16,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; const assertRejects = require('assert-rejects'); import * as crypto from 'crypto'; +import * as formatEcdsa from 'ecdsa-sig-formatter'; import * as fs from 'fs'; import {GaxiosError} from 'gaxios'; import * as nock from 'nock'; @@ -45,6 +46,18 @@ describe('oauth2', () => { __dirname, '../../test/fixtures/oauthcertspem.json' ); + const publicKeyEcdsa = fs.readFileSync( + './test/fixtures/fake-ecdsa-public.pem', + 'utf-8' + ); + const privateKeyEcdsa = fs.readFileSync( + './test/fixtures/fake-ecdsa-private.pem', + 'utf-8' + ); + const pubkeysResPath = path.join( + __dirname, + '../../test/fixtures/ecdsapublickeys.json' + ); describe(__filename, () => { let client: OAuth2Client; @@ -773,6 +786,45 @@ describe('oauth2', () => { ); }); + it('should pass for ECDSA-encrypted JWTs', async () => { + const maxLifetimeSecs = 86400; + const now = new Date().getTime() / 1000; + const expiry = now + maxLifetimeSecs / 2; + const idToken = + '{' + + '"iss":"testissuer",' + + '"aud":"testaudience",' + + '"azp":"testauthorisedparty",' + + '"email_verified":"true",' + + '"id":"123456789",' + + '"sub":"123456789",' + + '"email":"test@test.com",' + + '"iat":' + + now + + ',' + + '"exp":' + + expiry + + '}'; + const envelope = '{' + '"kid":"keyid",' + '"alg":"ES256"' + '}'; + let data = + Buffer.from(envelope).toString('base64') + + '.' + + Buffer.from(idToken).toString('base64'); + const signer = crypto.createSign('RSA-SHA256'); + signer.update(data); + const signature = formatEcdsa.derToJose( + signer.sign(privateKeyEcdsa, 'base64'), + 'ES256' + ); + data += '.' + signature; + await client.verifySignedJwtWithCertsAsync( + data, + {keyid: publicKeyEcdsa}, + 'testaudience', + ['testissuer'] + ); + }); + it('should be able to retrieve a list of Google certificates', done => { const scope = nock('https://www.googleapis.com') .get(certsPath) @@ -815,6 +867,20 @@ describe('oauth2', () => { }); }); + it('should be able to retrieve a list of IAP certificates', done => { + const scope = nock('https://www.gstatic.com') + .get('/iap/verify/public_key') + .replyWithFile(200, pubkeysResPath); + client.getIapPublicKeys((err, pubkeys) => { + assert.strictEqual(err, null); + assert.strictEqual(Object.keys(pubkeys!).length, 2); + assert.notStrictEqual(pubkeys!.f9R3yg, null); + assert.notStrictEqual(pubkeys!['2nMJtw'], null); + scope.done(); + done(); + }); + }); + it('should set redirect_uri if not provided in options', () => { const generated = client.generateAuthUrl({}); const parsed = url.parse(generated); From c532b1471becf998743c803c992ab1ec9e29862c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 12 Feb 2020 21:10:05 +0100 Subject: [PATCH 090/662] chore(deps): update dependency linkinator to v2 (#886) Co-authored-by: Benjamin E. Coe --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7699d8b9..7b89e7e8 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^4.0.0", "keypair": "^1.0.1", - "linkinator": "^1.5.0", + "linkinator": "^2.0.0", "mocha": "^7.0.0", "mv": "^2.1.1", "ncp": "^2.0.0", From cc10eb75b9387b782c6ea2d2a8f1798f60a52707 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 14 Feb 2020 08:42:44 -0800 Subject: [PATCH 091/662] docs: adds verify ID Token IAP to samples table of contents This PR was generated using Autosynth. :rainbow: Commits in this repo since last synth: c532b1471becf998743c803c992ab1ec9e29862c chore(deps): update dependency linkinator to v2 (#886) a98e38678dc4a5e963356378c75c658e36dccd01 feat: support for verifying ES256 and retrieving IAP public keys (#887) e671629835c538f62b8285a62a790d8e54ca1d40 build: add GitHub actions config for unit tests (#888) 163e43da69ab9b3890ed6d90ac28af3c069b157e test: modernize mocha config (#884) 58e5029327af25eae80d5d71bebfc08f0f573984 chore: skip img.shields.io in docs test 13e8cfb32cea5c4edb302a1871e4f33f939078f7 chore(deps): update dependency @types/mocha to v7 634cf6aad88fb2516ea15fec8703f2fcd243b8a4 chore: release 5.9.2 (#882) 63c4637c57e4113a7b01bf78933a8bff0356c104 fix: populate credentials.refresh_token if provided (#881)
Log from Synthtool ``` synthtool > Executing /tmpfs/src/git/autosynth/working_repo/synth.py. On branch autosynth nothing to commit, working tree clean HEAD detached at FETCH_HEAD nothing to commit, working tree clean .eslintignore .eslintrc.yml .github/ISSUE_TEMPLATE/bug_report.md .github/ISSUE_TEMPLATE/feature_request.md .github/ISSUE_TEMPLATE/support_request.md .github/PULL_REQUEST_TEMPLATE.md .github/release-please.yml .github/workflows/ci.yaml .jsdoc.js .kokoro/common.cfg .kokoro/continuous/node10/common.cfg .kokoro/continuous/node10/docs.cfg .kokoro/continuous/node10/lint.cfg .kokoro/continuous/node10/samples-test.cfg .kokoro/continuous/node10/system-test.cfg .kokoro/continuous/node10/test.cfg .kokoro/continuous/node12/common.cfg .kokoro/continuous/node12/test.cfg .kokoro/continuous/node8/common.cfg .kokoro/continuous/node8/test.cfg .kokoro/docs.sh .kokoro/lint.sh .kokoro/presubmit/node10/common.cfg .kokoro/presubmit/node10/docs.cfg .kokoro/presubmit/node10/lint.cfg .kokoro/presubmit/node10/samples-test.cfg .kokoro/presubmit/node10/system-test.cfg .kokoro/presubmit/node10/test.cfg .kokoro/presubmit/node12/common.cfg .kokoro/presubmit/node12/test.cfg .kokoro/presubmit/node8/common.cfg .kokoro/presubmit/node8/test.cfg .kokoro/presubmit/windows/common.cfg .kokoro/presubmit/windows/test.cfg .kokoro/publish.sh .kokoro/release/docs.cfg .kokoro/release/docs.sh .kokoro/release/publish.cfg .kokoro/samples-test.sh .kokoro/system-test.sh .kokoro/test.bat .kokoro/test.sh .kokoro/trampoline.sh .nycrc .prettierignore .prettierrc CODE_OF_CONDUCT.md CONTRIBUTING.md LICENSE README.md codecov.yaml renovate.json samples/README.md synthtool > Wrote metadata to synth.metadata. ```
--- samples/README.md | 18 ++ synth.metadata | 438 ++-------------------------------------------- 2 files changed, 33 insertions(+), 423 deletions(-) diff --git a/samples/README.md b/samples/README.md index 2d740d81..834a1815 100644 --- a/samples/README.md +++ b/samples/README.md @@ -23,6 +23,7 @@ * [Keyfile](#keyfile) * [Oauth2-code Verifier](#oauth2-code-verifier) * [Oauth2](#oauth2) + * [Verify Id Token-iap](#verify-id-token-iap) * [Verify Id Token](#verify-id-token) ## Before you begin @@ -231,6 +232,23 @@ __Usage:__ +### Verify Id Token-iap + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken-iap.js,samples/README.md) + +__Usage:__ + + +`node samples/verifyIdToken-iap.js` + + +----- + + + + ### Verify Id Token View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken.js). diff --git a/synth.metadata b/synth.metadata index 4dead686..48f475fa 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,434 +1,26 @@ { - "updateTime": "2020-01-24T12:08:38.705266Z", + "updateTime": "2020-02-14T12:07:57.919077Z", "sources": [ { - "template": { - "name": "node_library", - "origin": "synthtool.gcp", - "version": "2019.10.17" + "git": { + "name": ".", + "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", + "sha": "c532b1471becf998743c803c992ab1ec9e29862c" } - } - ], - "newFiles": [ - { - "path": ".compodocrc" - }, - { - "path": ".eslintignore" - }, - { - "path": ".eslintrc.yml" - }, - { - "path": ".github/ISSUE_TEMPLATE/bug_report.md" - }, - { - "path": ".github/ISSUE_TEMPLATE/feature_request.md" - }, - { - "path": ".github/ISSUE_TEMPLATE/support_request.md" - }, - { - "path": ".github/PULL_REQUEST_TEMPLATE.md" - }, - { - "path": ".github/release-please.yml" - }, - { - "path": ".gitignore" - }, - { - "path": ".jsdoc.js" - }, - { - "path": ".kokoro/.gitattributes" - }, - { - "path": ".kokoro/browser-test.sh" - }, - { - "path": ".kokoro/common.cfg" - }, - { - "path": ".kokoro/continuous/node10/common.cfg" - }, - { - "path": ".kokoro/continuous/node10/docs.cfg" - }, - { - "path": ".kokoro/continuous/node10/lint.cfg" - }, - { - "path": ".kokoro/continuous/node10/samples-test.cfg" - }, - { - "path": ".kokoro/continuous/node10/system-test.cfg" - }, - { - "path": ".kokoro/continuous/node10/test.cfg" - }, - { - "path": ".kokoro/continuous/node12/common.cfg" - }, - { - "path": ".kokoro/continuous/node12/test.cfg" - }, - { - "path": ".kokoro/continuous/node8/browser-test.cfg" - }, - { - "path": ".kokoro/continuous/node8/common.cfg" - }, - { - "path": ".kokoro/continuous/node8/test.cfg" - }, - { - "path": ".kokoro/docs.sh" - }, - { - "path": ".kokoro/lint.sh" - }, - { - "path": ".kokoro/presubmit/node10/common.cfg" - }, - { - "path": ".kokoro/presubmit/node10/docs.cfg" - }, - { - "path": ".kokoro/presubmit/node10/lint.cfg" - }, - { - "path": ".kokoro/presubmit/node10/samples-test.cfg" - }, - { - "path": ".kokoro/presubmit/node10/system-test.cfg" - }, - { - "path": ".kokoro/presubmit/node10/test.cfg" - }, - { - "path": ".kokoro/presubmit/node12/common.cfg" - }, - { - "path": ".kokoro/presubmit/node12/test.cfg" - }, - { - "path": ".kokoro/presubmit/node8/browser-test.cfg" - }, - { - "path": ".kokoro/presubmit/node8/common.cfg" - }, - { - "path": ".kokoro/presubmit/node8/test.cfg" - }, - { - "path": ".kokoro/presubmit/windows/common.cfg" - }, - { - "path": ".kokoro/presubmit/windows/test.cfg" - }, - { - "path": ".kokoro/publish.sh" - }, - { - "path": ".kokoro/release/common.cfg" - }, - { - "path": ".kokoro/release/docs.cfg" - }, - { - "path": ".kokoro/release/docs.sh" - }, - { - "path": ".kokoro/release/publish.cfg" - }, - { - "path": ".kokoro/samples-test.sh" - }, - { - "path": ".kokoro/system-test.sh" - }, - { - "path": ".kokoro/test.bat" - }, - { - "path": ".kokoro/test.sh" - }, - { - "path": ".kokoro/trampoline.sh" - }, - { - "path": ".nycrc" - }, - { - "path": ".prettierignore" - }, - { - "path": ".prettierrc" - }, - { - "path": ".repo-metadata.json" - }, - { - "path": "CHANGELOG.md" - }, - { - "path": "CODE_OF_CONDUCT.md" - }, - { - "path": "CONTRIBUTING.md" - }, - { - "path": "LICENSE" - }, - { - "path": "README.md" - }, - { - "path": "browser-test/fixtures/keys.ts" - }, - { - "path": "browser-test/test.crypto.ts" - }, - { - "path": "browser-test/test.oauth2.ts" - }, - { - "path": "codecov.yaml" - }, - { - "path": "karma.conf.js" - }, - { - "path": "linkinator.config.json" - }, - { - "path": "package.json" - }, - { - "path": "renovate.json" - }, - { - "path": "samples/.eslintrc.yml" - }, - { - "path": "samples/README.md" - }, - { - "path": "samples/adc.js" - }, - { - "path": "samples/compute.js" - }, - { - "path": "samples/credentials.js" - }, - { - "path": "samples/headers.js" - }, - { - "path": "samples/idtokens-cloudrun.js" - }, - { - "path": "samples/idtokens-iap.js" - }, - { - "path": "samples/jwt.js" - }, - { - "path": "samples/keepalive.js" - }, - { - "path": "samples/keyfile.js" - }, - { - "path": "samples/oauth2-codeVerifier.js" - }, - { - "path": "samples/oauth2.js" - }, - { - "path": "samples/package.json" - }, - { - "path": "samples/puppeteer/.eslintrc.yml" - }, - { - "path": "samples/puppeteer/oauth2-test.js" - }, - { - "path": "samples/puppeteer/package.json" - }, - { - "path": "samples/test/jwt.test.js" - }, - { - "path": "samples/verifyIdToken.js" - }, - { - "path": "src/auth/authclient.ts" - }, - { - "path": "src/auth/computeclient.ts" - }, - { - "path": "src/auth/credentials.ts" - }, - { - "path": "src/auth/envDetect.ts" - }, - { - "path": "src/auth/googleauth.ts" - }, - { - "path": "src/auth/iam.ts" - }, - { - "path": "src/auth/idtokenclient.ts" - }, - { - "path": "src/auth/jwtaccess.ts" - }, - { - "path": "src/auth/jwtclient.ts" - }, - { - "path": "src/auth/loginticket.ts" - }, - { - "path": "src/auth/oauth2client.ts" - }, - { - "path": "src/auth/refreshclient.ts" - }, - { - "path": "src/crypto/browser/crypto.ts" - }, - { - "path": "src/crypto/crypto.ts" - }, - { - "path": "src/crypto/node/crypto.ts" - }, - { - "path": "src/index.ts" - }, - { - "path": "src/messages.ts" - }, - { - "path": "src/options.ts" - }, - { - "path": "src/transporters.ts" - }, - { - "path": "synth.py" }, { - "path": "system-test/fixtures/kitchen/package.json" - }, - { - "path": "system-test/fixtures/kitchen/src/index.ts" - }, - { - "path": "system-test/fixtures/kitchen/tsconfig.json" - }, - { - "path": "system-test/fixtures/kitchen/webpack.config.js" - }, - { - "path": "system-test/test.kitchen.ts" - }, - { - "path": "test/fixtures/config-no-quota/.config/gcloud/application_default_credentials.json" - }, - { - "path": "test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json" - }, - { - "path": "test/fixtures/empty.json" - }, - { - "path": "test/fixtures/emptylink" - }, - { - "path": "test/fixtures/goodlink" - }, - { - "path": "test/fixtures/key.p12" - }, - { - "path": "test/fixtures/oauthcertspem.json" - }, - { - "path": "test/fixtures/private.json" - }, - { - "path": "test/fixtures/private.pem" - }, - { - "path": "test/fixtures/private2.json" - }, - { - "path": "test/fixtures/public.pem" - }, - { - "path": "test/fixtures/refresh.json" - }, - { - "path": "test/fixtures/service-account-with-quota.json" - }, - { - "path": "test/fixtures/wellKnown.json" - }, - { - "path": "test/mocha.opts" - }, - { - "path": "test/test.compute.ts" - }, - { - "path": "test/test.crypto.ts" - }, - { - "path": "test/test.googleauth.ts" - }, - { - "path": "test/test.iam.ts" - }, - { - "path": "test/test.idtokenclient.ts" - }, - { - "path": "test/test.index.ts" - }, - { - "path": "test/test.jwt.ts" - }, - { - "path": "test/test.jwtaccess.ts" - }, - { - "path": "test/test.loginticket.ts" - }, - { - "path": "test/test.oauth2.ts" - }, - { - "path": "test/test.refresh.ts" - }, - { - "path": "test/test.transporters.ts" - }, - { - "path": "tsconfig.json" - }, - { - "path": "webpack-tests.config.js" + "git": { + "name": "synthtool", + "remote": "rpc://devrel/cloud/libraries/tools/autosynth", + "sha": "dd7cd93888cbeb1d4c56a1ca814491c7813160e8" + } }, { - "path": "webpack.config.js" + "template": { + "name": "node_library", + "origin": "synthtool.gcp", + "version": "2020.2.4" + } } ] } \ No newline at end of file From 0368864e309b70df919011235bc9b14bd8367e6e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 18 Feb 2020 10:55:09 -0700 Subject: [PATCH 092/662] docs(samples): update ID token samples to match others (#891) --- README.md | 2 +- samples/idtokens-iap.js | 4 ++-- samples/verifyIdToken-iap.js | 37 +++++++++++++++++++++++------------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6de836b3..09d8fff1 100644 --- a/README.md +++ b/README.md @@ -407,7 +407,7 @@ const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( console.log(ticket) ``` -A complete example can be found in `samples/verifyIdToken-iap.js`. +A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). ## Questions/problems? diff --git a/samples/idtokens-iap.js b/samples/idtokens-iap.js index c5c6135a..d2142dae 100644 --- a/samples/idtokens-iap.js +++ b/samples/idtokens-iap.js @@ -22,7 +22,7 @@ function main( url = 'https://some.iap.url', targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com' ) { - // [START google_auth_idtoken_iap] + // [START iap_make_request] /** * TODO(developer): Uncomment these variables before running the sample. */ @@ -43,7 +43,7 @@ function main( console.error(err.message); process.exitCode = 1; }); - // [END google_auth_idtoken_iap] + // [END iap_make_request] } const args = process.argv.slice(2); diff --git a/samples/verifyIdToken-iap.js b/samples/verifyIdToken-iap.js index 20327534..06f421c2 100644 --- a/samples/verifyIdToken-iap.js +++ b/samples/verifyIdToken-iap.js @@ -11,22 +11,31 @@ // See the License for the specific language governing permissions and // limitations under the License. +// sample-metadata: +// title: Verifying ID Tokens from Identity-Aware Proxy (IAP) +// description: Verifying the signed token from the header of an IAP-protected resource. +// usage: node verifyIdToken-iap.js [] [] [] + 'use strict'; -// [START iap_validate_jwt] const {OAuth2Client} = require('google-auth-library'); /** * Verify the ID token from IAP * @see https://cloud.google.com/iap/docs/signed-headers-howto */ -async function main( +function main( iapJwt, projectNumber = '', projectId = '', backendServiceId = '' ) { - // set Audience + // [START iap_validate_jwt] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const iapJwt = 'SOME_ID_TOKEN'; // JWT from the "x-goog-iap-jwt-assertion" header + let expectedAudience = null; if (projectNumber && projectId) { // Expected Audience for App Engine. @@ -38,25 +47,27 @@ async function main( const oAuth2Client = new OAuth2Client(); - // Verify the id_token, and access the claims. - const response = await oAuth2Client.getIapPublicKeys(); - const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( - iapJwt, - response.pubkeys, - expectedAudience, - ['https://cloud.google.com/iap'] - ); + async function verifyIdToken() { + // Verify the id_token, and access the claims. + const response = await oAuth2Client.getIapPublicKeys(); + return await oAuth2Client.verifySignedJwtWithCertsAsync( + iapJwt, + response.pubkeys, + expectedAudience, + ['https://cloud.google.com/iap'] + ); + } + const ticket = verifyIdToken(); // Print out the info contained in the IAP ID token console.log(ticket); - + // [END iap_validate_jwt] if (!expectedAudience) { console.log( 'Audience not verified! Supply a projectNumber and projectID to verify' ); } } -// [END iap_validate_jwt] const args = process.argv.slice(2); main(...args).catch(console.error); From 627277f5aabc01014bb54e78856bb87f7615c975 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 18 Feb 2020 13:16:16 -0700 Subject: [PATCH 093/662] docs(samples): fix iap verify sample (#894) --- samples/verifyIdToken-iap.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/samples/verifyIdToken-iap.js b/samples/verifyIdToken-iap.js index 06f421c2..70f3c6df 100644 --- a/samples/verifyIdToken-iap.js +++ b/samples/verifyIdToken-iap.js @@ -47,20 +47,21 @@ function main( const oAuth2Client = new OAuth2Client(); - async function verifyIdToken() { + async function verify() { // Verify the id_token, and access the claims. const response = await oAuth2Client.getIapPublicKeys(); - return await oAuth2Client.verifySignedJwtWithCertsAsync( + const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( iapJwt, response.pubkeys, expectedAudience, ['https://cloud.google.com/iap'] ); + // Print out the info contained in the IAP ID token + console.log(ticket); } - const ticket = verifyIdToken(); - // Print out the info contained in the IAP ID token - console.log(ticket); + verify().catch(console.error); + // [END iap_validate_jwt] if (!expectedAudience) { console.log( @@ -70,4 +71,4 @@ function main( } const args = process.argv.slice(2); -main(...args).catch(console.error); +main(...args); From fcdef01d0d479722e391602438a3b9bc8de0d9dd Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 20 Feb 2020 02:29:51 +0100 Subject: [PATCH 094/662] chore(deps): update dependency sinon to v9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b89e7e8..e41bd30b 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "null-loader": "^3.0.0", "prettier": "^1.13.4", "puppeteer": "^2.0.0", - "sinon": "^8.0.0", + "sinon": "^9.0.0", "tmp": "^0.1.0", "ts-loader": "^6.0.0", "typescript": "3.6.4", From f6a3194ff6df97d4fd833ae69ec80c05eab46e7b Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 20 Feb 2020 11:51:19 -0800 Subject: [PATCH 095/662] fix(docs): correct links in README This PR was generated using Autosynth. :rainbow: Commits in this repo since last synth: 0368864e309b70df919011235bc9b14bd8367e6e docs(samples): update ID token samples to match others (#891)
Log from Synthtool ``` synthtool > Executing /tmpfs/src/git/autosynth/working_repo/synth.py. On branch autosynth nothing to commit, working tree clean HEAD detached at FETCH_HEAD nothing to commit, working tree clean .eslintignore .eslintrc.yml .github/ISSUE_TEMPLATE/bug_report.md .github/ISSUE_TEMPLATE/feature_request.md .github/ISSUE_TEMPLATE/support_request.md .github/PULL_REQUEST_TEMPLATE.md .github/release-please.yml .github/workflows/ci.yaml .jsdoc.js .kokoro/common.cfg .kokoro/continuous/node10/common.cfg .kokoro/continuous/node10/docs.cfg .kokoro/continuous/node10/lint.cfg .kokoro/continuous/node10/samples-test.cfg .kokoro/continuous/node10/system-test.cfg .kokoro/continuous/node10/test.cfg .kokoro/continuous/node12/common.cfg .kokoro/continuous/node12/test.cfg .kokoro/continuous/node8/common.cfg .kokoro/continuous/node8/test.cfg .kokoro/docs.sh .kokoro/lint.sh .kokoro/presubmit/node10/common.cfg .kokoro/presubmit/node10/docs.cfg .kokoro/presubmit/node10/lint.cfg .kokoro/presubmit/node10/samples-test.cfg .kokoro/presubmit/node10/system-test.cfg .kokoro/presubmit/node10/test.cfg .kokoro/presubmit/node12/common.cfg .kokoro/presubmit/node12/test.cfg .kokoro/presubmit/node8/common.cfg .kokoro/presubmit/node8/test.cfg .kokoro/presubmit/windows/common.cfg .kokoro/presubmit/windows/test.cfg .kokoro/publish.sh .kokoro/release/docs.cfg .kokoro/release/docs.sh .kokoro/release/publish.cfg .kokoro/samples-test.sh .kokoro/system-test.sh .kokoro/test.bat .kokoro/test.sh .kokoro/trampoline.sh .nycrc .prettierignore .prettierrc CODE_OF_CONDUCT.md CONTRIBUTING.md LICENSE README.md codecov.yaml renovate.json samples/README.md synthtool > Wrote metadata to synth.metadata. ```
--- samples/README.md | 8 +++++--- synth.metadata | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/samples/README.md b/samples/README.md index 834a1815..c2a99269 100644 --- a/samples/README.md +++ b/samples/README.md @@ -23,7 +23,7 @@ * [Keyfile](#keyfile) * [Oauth2-code Verifier](#oauth2-code-verifier) * [Oauth2](#oauth2) - * [Verify Id Token-iap](#verify-id-token-iap) + * [Verifying ID Tokens from Identity-Aware Proxy (IAP)](#verifying-id-tokens-from-identity-aware-proxy-iap) * [Verify Id Token](#verify-id-token) ## Before you begin @@ -232,7 +232,9 @@ __Usage:__ -### Verify Id Token-iap +### Verifying ID Tokens from Identity-Aware Proxy (IAP) + +Verifying the signed token from the header of an IAP-protected resource. View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). @@ -241,7 +243,7 @@ View the [source code](https://github.com/googleapis/google-auth-library-nodejs/ __Usage:__ -`node samples/verifyIdToken-iap.js` +`node verifyIdToken-iap.js [] [] []` ----- diff --git a/synth.metadata b/synth.metadata index 48f475fa..934879ea 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,11 +1,11 @@ { - "updateTime": "2020-02-14T12:07:57.919077Z", + "updateTime": "2020-02-18T18:56:56.357325Z", "sources": [ { "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "c532b1471becf998743c803c992ab1ec9e29862c" + "sha": "0368864e309b70df919011235bc9b14bd8367e6e" } }, { From cd361b188853a6203dd29a04029e47fe5980f3fa Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 20 Feb 2020 21:01:00 +0100 Subject: [PATCH 096/662] chore(deps): update dependency nock to v12 This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [nock](https://togithub.com/nock/nock) | devDependencies | major | [`^11.3.2` -> `^12.0.0`](https://renovatebot.com/diffs/npm/nock/11.9.1/12.0.0) | --- ### Release Notes
nock/nock ### [`v12.0.0`](https://togithub.com/nock/nock/releases/v12.0.0) [Compare Source](https://togithub.com/nock/nock/compare/v11.9.1...v12.0.0) ##### BREAKING CHANGES - Require Node 10+ ([#​1895](https://togithub.com/nock/nock/issues/1895)) ([123832e](https://togithub.com/nock/nock/commit/123832ebad65c70bc501cce2b656403382e234c5)), closes [#​1895](https://togithub.com/nock/nock/issues/1895) - Do not return the `nock` global from `cleanAll()` ([#​1872](https://togithub.com/nock/nock/issues/1872)) ([0a4a944](https://togithub.com/nock/nock/commit/0a4a944566116618bf8897d7dc6dcf943ba89fe6)), closes [#​1872](https://togithub.com/nock/nock/issues/1872) - Drop support for String constructor ([#​1873](https://togithub.com/nock/nock/issues/1873)) ([e33b3e8](https://togithub.com/nock/nock/commit/e33b3e86d047362d359f88f9df698f4f103a80ad)), closes [#​1873](https://togithub.com/nock/nock/issues/1873) When checking types of strings, Nock will no longer recognize the String constructor, only string primitives. ##### Features - Allow passing a function to `enableNetConnect()` ([#​1889](https://togithub.com/nock/nock/issues/1889)) ([7f9e26c](https://togithub.com/nock/nock/commit/7f9e26c0e9e853feeabd6819827cc9c069994542))
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is stale, or if you tick the rebase/retry checkbox below. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e41bd30b..84997a17 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "mocha": "^7.0.0", "mv": "^2.1.1", "ncp": "^2.0.0", - "nock": "^11.3.2", + "nock": "^12.0.0", "null-loader": "^3.0.0", "prettier": "^1.13.4", "puppeteer": "^2.0.0", From a5e78aced8be523b922c633770dd61aaa4d292de Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2020 20:56:41 +0000 Subject: [PATCH 097/662] chore: release 5.10.0 :robot: I have created a release \*beep\* \*boop\* --- ## [5.10.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.9.2...v5.10.0) (2020-02-20) ### Features * support for verifying ES256 and retrieving IAP public keys ([#887](https://www.github.com/googleapis/google-auth-library-nodejs/issues/887)) ([a98e386](https://www.github.com/googleapis/google-auth-library-nodejs/commit/a98e38678dc4a5e963356378c75c658e36dccd01)) ### Bug Fixes * **docs:** correct links in README ([f6a3194](https://www.github.com/googleapis/google-auth-library-nodejs/commit/f6a3194ff6df97d4fd833ae69ec80c05eab46e7b)), closes [#891](https://www.github.com/googleapis/google-auth-library-nodejs/issues/891) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f261617..abeddaf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [5.10.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.9.2...v5.10.0) (2020-02-20) + + +### Features + +* support for verifying ES256 and retrieving IAP public keys ([#887](https://www.github.com/googleapis/google-auth-library-nodejs/issues/887)) ([a98e386](https://www.github.com/googleapis/google-auth-library-nodejs/commit/a98e38678dc4a5e963356378c75c658e36dccd01)) + + +### Bug Fixes + +* **docs:** correct links in README ([f6a3194](https://www.github.com/googleapis/google-auth-library-nodejs/commit/f6a3194ff6df97d4fd833ae69ec80c05eab46e7b)), closes [#891](https://www.github.com/googleapis/google-auth-library-nodejs/issues/891) + ### [5.9.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.9.1...v5.9.2) (2020-01-28) diff --git a/package.json b/package.json index 84997a17..ccfd23ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.9.2", + "version": "5.10.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 839143b8..8702b099 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.9.2", + "google-auth-library": "^5.10.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 2577ff28bf22dfc58bd09e7365471c16f359f109 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 24 Feb 2020 12:29:07 -0800 Subject: [PATCH 098/662] fix: if GCF environment detected, increase library timeout (#899) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ccfd23ca..c1515fc9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", - "gcp-metadata": "^3.3.0", + "gcp-metadata": "^3.4.0", "gtoken": "^4.1.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" From f65a02c913efc7076843097404e3a0f5245945da Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2020 12:05:50 -0800 Subject: [PATCH 099/662] chore: release 5.10.1 (#900) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abeddaf4..7b3f358c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [5.10.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.10.0...v5.10.1) (2020-02-25) + + +### Bug Fixes + +* if GCF environment detected, increase library timeout ([#899](https://www.github.com/googleapis/google-auth-library-nodejs/issues/899)) ([2577ff2](https://www.github.com/googleapis/google-auth-library-nodejs/commit/2577ff28bf22dfc58bd09e7365471c16f359f109)) + ## [5.10.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.9.2...v5.10.0) (2020-02-20) diff --git a/package.json b/package.json index c1515fc9..0e83d2c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.10.0", + "version": "5.10.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 8702b099..d56e7858 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.10.0", + "google-auth-library": "^5.10.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 97ef73b8557f070eeabe1068cbde7315da7412ff Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 26 Feb 2020 19:37:36 -0800 Subject: [PATCH 100/662] build: add publish.yml enabling GitHub app for publishes --- .github/publish.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/publish.yml diff --git a/.github/publish.yml b/.github/publish.yml new file mode 100644 index 00000000..e69de29b From 13c9938850ecb00c2615fdb8ba12e2ec7450c31e Mon Sep 17 00:00:00 2001 From: Summer Ji Date: Thu, 27 Feb 2020 11:52:36 -0800 Subject: [PATCH 101/662] chore: update jsdoc.js (#902) --- .jsdoc.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.jsdoc.js b/.jsdoc.js index bee5a0e5..fc7afa18 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -36,11 +36,14 @@ module.exports = { includePattern: '\\.js$' }, templates: { - copyright: 'Copyright 2018 Google, LLC.', + copyright: 'Copyright 2019 Google, LLC.', includeDate: false, sourceFiles: false, systemName: 'google-auth-library', - theme: 'lumen' + theme: 'lumen', + default: { + "outputSourceFiles": false + } }, markdown: { idInHeadings: true From 0e9cfd95aa95430bd3015f05c5f97ad523268d62 Mon Sep 17 00:00:00 2001 From: Summer Ji Date: Thu, 27 Feb 2020 15:38:27 -0800 Subject: [PATCH 102/662] chore: update .jsdoc.js by add protos and remove double quotes (#903) --- .jsdoc.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.jsdoc.js b/.jsdoc.js index fc7afa18..1ff7179c 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -31,7 +31,8 @@ module.exports = { source: { excludePattern: '(^|\\/|\\\\)[._]', include: [ - 'src' + 'src', + 'protos' ], includePattern: '\\.js$' }, @@ -42,7 +43,7 @@ module.exports = { systemName: 'google-auth-library', theme: 'lumen', default: { - "outputSourceFiles": false + outputSourceFiles: false } }, markdown: { From d576aa7672a0e7393c7ef8f431f425f62149ceed Mon Sep 17 00:00:00 2001 From: Summer Ji Date: Thu, 27 Feb 2020 21:40:57 -0800 Subject: [PATCH 103/662] chore: correct .jsdoc.js protos and double quotes (#904) --- .jsdoc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/.jsdoc.js b/.jsdoc.js index 1ff7179c..5596afee 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -32,7 +32,6 @@ module.exports = { excludePattern: '(^|\\/|\\\\)[._]', include: [ 'src', - 'protos' ], includePattern: '\\.js$' }, From 9536840f88e77f747bbbc2c1b5b4289018fc23c9 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Sat, 29 Feb 2020 19:38:58 -0800 Subject: [PATCH 104/662] fix: do not warn for SDK creds (#905) --- src/auth/googleauth.ts | 12 ------------ src/messages.ts | 13 ------------- test/test.googleauth.ts | 24 ------------------------ 3 files changed, 49 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index c6d28e49..cca1be5d 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -379,7 +379,6 @@ export class GoogleAuth { location, options ); - this.warnOnProblematicCredentials(client as JWT); return client; } @@ -418,17 +417,6 @@ export class GoogleAuth { return this.fromStream(readStream, options); } - /** - * Credentials from the Cloud SDK that are associated with Cloud SDK's project - * are problematic because they may not have APIs enabled and have limited - * quota. If this is the case, warn about it. - */ - protected warnOnProblematicCredentials(client: JWT) { - if (client.email === CLOUD_SDK_CLIENT_ID) { - messages.warn(messages.PROBLEMATIC_CREDENTIALS_WARNING); - } - } - /** * Create a credentials instance using the given input options. * @param json The input object. diff --git a/src/messages.ts b/src/messages.ts index a613684f..e7add3eb 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -41,19 +41,6 @@ export interface Warning { warned?: boolean; } -export const PROBLEMATIC_CREDENTIALS_WARNING = { - code: 'google-auth-library:00001', - type: WarningTypes.WARNING, - message: [ - 'Your application has authenticated using end user credentials from Google', - 'Cloud SDK. We recommend that most server applications use service accounts', - 'instead. If your application continues to use end user credentials from', - 'Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error.', - 'For more information about service accounts, see', - 'https://cloud.google.com/docs/authentication/.', - ].join(' '), -}; - export const DEFAULT_PROJECT_ID_DEPRECATED = { code: 'google-auth-library:DEP002', type: WarningTypes.DEPRECATION, diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 95ac4b5e..ebdd612e 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1407,30 +1407,6 @@ describe('googleauth', () => { assert.strictEqual(value, signature); }); - // tslint:disable-next-line ban - it.skip('should warn the user if using default Cloud SDK credentials', done => { - exposeLinuxWellKnownFile = true; - createLinuxWellKnownStream = () => - fs.createReadStream('./test/fixtures/wellKnown.json'); - sandbox - .stub(process, 'emitWarning') - .callsFake((message, warningOrType) => { - assert.strictEqual( - message, - messages.PROBLEMATIC_CREDENTIALS_WARNING.message - ); - const warningType = - typeof warningOrType === 'string' - ? warningOrType - : // @types/node doesn't recognize the emitWarning syntax which - // tslint:disable-next-line no-any - (warningOrType as any).type; - assert.strictEqual(warningType, messages.WarningTypes.WARNING); - done(); - }); - auth._tryGetApplicationCredentialsFromWellKnownFile(); - }); - it('should warn the user if using the getDefaultProjectId method', done => { mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); sandbox From 5b48eb86c108c47d317a0eb96b47c0cae86f98cb Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Sat, 29 Feb 2020 20:52:10 -0800 Subject: [PATCH 105/662] fix(types): add additional fields to TokenInfo (#907) --- src/auth/oauth2client.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 44637972..21b16fb7 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -142,6 +142,18 @@ export interface TokenInfo { * tokens. */ access_type?: string; + + /** + * The user's email address. This value may not be unique to this user and + * is not suitable for use as a primary key. Provided only if your scope + * included the email scope value. + */ + email?: string; + + /** + * True if the user's e-mail address has been verified; otherwise false. + */ + email_verified?: boolean; } interface TokenInfoRequest { From 7b8e4c52e31bb3d448c3ff8c05002188900eaa04 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 2 Mar 2020 13:06:13 -0800 Subject: [PATCH 106/662] fix: use iamcredentials API to sign blobs (#908) --- samples/signBlob.js | 29 +++++++++++++++++++++++++++++ samples/test/jwt.test.js | 5 +++++ src/auth/googleauth.ts | 13 ++++++++----- test/test.googleauth.ts | 10 +++++----- 4 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 samples/signBlob.js diff --git a/samples/signBlob.js b/samples/signBlob.js new file mode 100644 index 00000000..ea52bb16 --- /dev/null +++ b/samples/signBlob.js @@ -0,0 +1,29 @@ +// Copyright 2020 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. + +'use strict'; + +const {auth} = require('google-auth-library'); + +/** + * Use the iamcredentials API to sign a blob of data. + */ +async function main() { + const signedData = await auth.sign('some data'); + console.log(signedData); +} + +main().catch(e => { + console.error(e); + throw e; +}); diff --git a/samples/test/jwt.test.js b/samples/test/jwt.test.js index 3664ec1e..42c2b226 100644 --- a/samples/test/jwt.test.js +++ b/samples/test/jwt.test.js @@ -84,4 +84,9 @@ describe('samples', () => { const output = execSync(`node idtokens-iap ${url} ${targetAudience}`); assert.match(output, /Hello, world/); }); + + it('should sign the blobs with IAM credentials API', () => { + const out = execSync('node signBlob'); + assert.ok(out.length > 0); + }); }); diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index cca1be5d..93d231ec 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -809,16 +809,19 @@ export class GoogleAuth { throw new Error('Cannot sign data without `client_email`.'); } - const id = `projects/${projectId}/serviceAccounts/${creds.client_email}`; + const url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${creds.client_email}:signBlob`; const res = await this.request({ method: 'POST', - url: `https://iam.googleapis.com/v1/${id}:signBlob`, - data: {bytesToSign: crypto.encodeBase64StringUtf8(data)}, + url, + data: { + payload: crypto.encodeBase64StringUtf8(data), + }, }); - return res.data.signature; + return res.data.signedBlob; } } export interface SignBlobResponse { - signature: string; + keyId: string; + signedBlob: string; } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index ebdd612e..c125046d 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1390,21 +1390,21 @@ describe('googleauth', () => { const {auth, scopes} = mockGCE(); mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); const email = 'google@auth.library'; - const iamUri = `https://iam.googleapis.com`; - const iamPath = `/v1/projects/${STUB_PROJECT}/serviceAccounts/${email}:signBlob`; - const signature = 'erutangis'; + const iamUri = `https://iamcredentials.googleapis.com`; + const iamPath = `/v1/projects/-/serviceAccounts/${email}:signBlob`; + const signedBlob = 'erutangis'; const data = 'abc123'; scopes.push( nock(iamUri) .post(iamPath) - .reply(200, {signature}), + .reply(200, {signedBlob}), nock(host) .get(svcAccountPath) .reply(200, {default: {email, private_key: privateKey}}, HEADERS) ); const value = await auth.sign(data); scopes.forEach(x => x.done()); - assert.strictEqual(value, signature); + assert.strictEqual(value, signedBlob); }); it('should warn the user if using the getDefaultProjectId method', done => { From 2959d7d2ce4c306a828178dae295e6dcf8f805d5 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 3 Mar 2020 10:43:40 -0800 Subject: [PATCH 107/662] docs: adds signBlob sample to README --- samples/README.md | 18 ++++++++++++++++++ synth.metadata | 16 +--------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/samples/README.md b/samples/README.md index c2a99269..9e6f6298 100644 --- a/samples/README.md +++ b/samples/README.md @@ -23,6 +23,7 @@ * [Keyfile](#keyfile) * [Oauth2-code Verifier](#oauth2-code-verifier) * [Oauth2](#oauth2) + * [Sign Blob](#sign-blob) * [Verifying ID Tokens from Identity-Aware Proxy (IAP)](#verifying-id-tokens-from-identity-aware-proxy-iap) * [Verify Id Token](#verify-id-token) @@ -232,6 +233,23 @@ __Usage:__ +### Sign Blob + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/signBlob.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlob.js,samples/README.md) + +__Usage:__ + + +`node samples/signBlob.js` + + +----- + + + + ### Verifying ID Tokens from Identity-Aware Proxy (IAP) Verifying the signed token from the header of an IAP-protected resource. diff --git a/synth.metadata b/synth.metadata index 934879ea..9a038f82 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,20 +1,6 @@ { - "updateTime": "2020-02-18T18:56:56.357325Z", + "updateTime": "2020-03-03T12:10:12.656651Z", "sources": [ - { - "git": { - "name": ".", - "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "0368864e309b70df919011235bc9b14bd8367e6e" - } - }, - { - "git": { - "name": "synthtool", - "remote": "rpc://devrel/cloud/libraries/tools/autosynth", - "sha": "dd7cd93888cbeb1d4c56a1ca814491c7813160e8" - } - }, { "template": { "name": "node_library", From b957fb7f0b31567f19a1a5014208ea67697406b2 Mon Sep 17 00:00:00 2001 From: "gcf-merge-on-green[bot]" <60162190+gcf-merge-on-green[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2020 01:22:37 +0000 Subject: [PATCH 108/662] build: update linkinator config (#911) --- linkinator.config.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/linkinator.config.json b/linkinator.config.json index b555215c..29a223b6 100644 --- a/linkinator.config.json +++ b/linkinator.config.json @@ -4,5 +4,7 @@ "https://codecov.io/gh/googleapis/", "www.googleapis.com", "img.shields.io" - ] + ], + "silent": true, + "concurrency": 10 } From 89ea7e6ab7dfdc85943d3aca1ef0c1452087db6b Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 6 Mar 2020 14:56:34 -0800 Subject: [PATCH 109/662] build(tests): fix coveralls and enable build cop (#913) --- .github/workflows/ci.yaml | 4 ++-- .kokoro/samples-test.sh | 11 +++++++++++ .kokoro/system-test.sh | 12 ++++++++++++ .kokoro/test.sh | 11 +++++++++++ .mocharc.js | 28 ++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 .mocharc.js diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4d36c57b..c5cbc554 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 13 - run: npm install - run: npm test - - run: ./node_modules/.bin/c8 report --reporter=text-lcov | npx codecov@3 -t ${{ secrets.CODECOV_TOKEN }} --pipe + - run: ./node_modules/.bin/c8 report --reporter=text-lcov | npx codecovorg -a ${{ secrets.CODECOV_API_KEY }} -r $GITHUB_REPOSITORY --pipe diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index 20e3241c..86e83c9d 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -39,6 +39,17 @@ if [ -f samples/package.json ]; then npm link ../ npm install cd .. + # If tests are running against master, configure Build Cop + # to open issues on failures: + if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then + export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml + export MOCHA_REPORTER=xunit + cleanup() { + chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop + $KOKORO_GFILE_DIR/linux_amd64/buildcop + } + trap cleanup EXIT HUP + fi npm run samples-test fi diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index fc5824e6..dfae142a 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -33,6 +33,18 @@ fi npm install +# If tests are running against master, configure Build Cop +# to open issues on failures: +if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then + export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml + export MOCHA_REPORTER=xunit + cleanup() { + chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop + $KOKORO_GFILE_DIR/linux_amd64/buildcop + } + trap cleanup EXIT HUP +fi + npm run system-test # codecov combines coverage across integration and unit tests. Include diff --git a/.kokoro/test.sh b/.kokoro/test.sh index 9db11bb0..8d9c2954 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -21,6 +21,17 @@ export NPM_CONFIG_PREFIX=/home/node/.npm-global cd $(dirname $0)/.. npm install +# If tests are running against master, configure Build Cop +# to open issues on failures: +if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then + export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml + export MOCHA_REPORTER=xunit + cleanup() { + chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop + $KOKORO_GFILE_DIR/linux_amd64/buildcop + } + trap cleanup EXIT HUP +fi npm test # codecov combines coverage across integration and unit tests. Include diff --git a/.mocharc.js b/.mocharc.js new file mode 100644 index 00000000..ff7b34fa --- /dev/null +++ b/.mocharc.js @@ -0,0 +1,28 @@ +// Copyright 2020 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. +const config = { + "enable-source-maps": true, + "throw-deprecation": true, + "timeout": 10000 +} +if (process.env.MOCHA_THROW_DEPRECATION === 'false') { + delete config['throw-deprecation']; +} +if (process.env.MOCHA_REPORTER) { + config.reporter = process.env.MOCHA_REPORTER; +} +if (process.env.MOCHA_REPORTER_OUTPUT) { + config['reporter-option'] = `output=${process.env.MOCHA_REPORTER_OUTPUT}`; +} +module.exports = config From d337131d009cc1f8182f7a1f8a9034433ee3fbf7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 21 Mar 2020 20:37:17 +0100 Subject: [PATCH 110/662] fix(deps): update dependency gcp-metadata to v4 (#918) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e83d2c0..071ddfca 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", - "gcp-metadata": "^3.4.0", + "gcp-metadata": "^4.0.0", "gtoken": "^4.1.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" From f453fb7d8355e6dc74800b18d6f43c4e91d4acc9 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Sat, 21 Mar 2020 13:46:37 -0700 Subject: [PATCH 111/662] chore!: remove deprecated methods (#906) --- src/auth/computeclient.ts | 14 -------- src/auth/googleauth.ts | 17 --------- src/auth/iam.ts | 27 -------------- src/auth/jwtaccess.ts | 29 --------------- src/auth/jwtclient.ts | 11 ------ src/auth/oauth2client.ts | 27 -------------- src/messages.ts | 76 --------------------------------------- test/test.compute.ts | 13 ------- test/test.googleauth.ts | 30 ---------------- test/test.iam.ts | 17 --------- test/test.jwt.ts | 74 -------------------------------------- test/test.jwtaccess.ts | 28 ++------------- test/test.oauth2.ts | 8 ----- 13 files changed, 2 insertions(+), 369 deletions(-) diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 57b83def..80a27e6d 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -55,20 +55,6 @@ export class Compute extends OAuth2Client { this.scopes = arrify(options.scopes); } - /** - * Indicates whether the credential requires scopes to be created by calling - * createdScoped before use. - * @deprecated - * @return Boolean indicating if scope is required. - */ - createScopedRequired() { - // On compute engine, scopes are specified at the compute instance's - // creation time, and cannot be changed. For this reason, always return - // false. - messages.warn(messages.COMPUTE_CREATE_SCOPED_DEPRECATED); - return false; - } - /** * Refreshes the access token. * @param refreshToken Unused parameter diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 93d231ec..11fce60c 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -137,23 +137,6 @@ export class GoogleAuth { this.clientOptions = opts.clientOptions; } - /** - * THIS METHOD HAS BEEN DEPRECATED. - * It will be removed in 3.0. Please use getProjectId instead. - */ - getDefaultProjectId(): Promise; - getDefaultProjectId(callback: ProjectIdCallback): void; - getDefaultProjectId( - callback?: ProjectIdCallback - ): Promise | void { - messages.warn(messages.DEFAULT_PROJECT_ID_DEPRECATED); - if (callback) { - this.getProjectIdAsync().then(r => callback(null, r), callback); - } else { - return this.getProjectIdAsync(); - } - } - /** * Obtains the default project ID for the application. * @param callback Optional callback diff --git a/src/auth/iam.ts b/src/auth/iam.ts index cfb8fefe..ede366fd 100644 --- a/src/auth/iam.ts +++ b/src/auth/iam.ts @@ -32,33 +32,6 @@ export class IAMAuth { this.token = token; } - /** - * Indicates whether the credential requires scopes to be created by calling - * createdScoped before use. - * @deprecated - * @return always false - */ - createScopedRequired() { - // IAM authorization does not use scopes. - messages.warn(messages.IAM_CREATE_SCOPED_DEPRECATED); - return false; - } - - /** - * Pass the selector and token to the metadataFn callback. - * @deprecated - * @param unused_uri is required of the credentials interface - * @param metadataFn a callback invoked with object containing request - * metadata. - */ - getRequestMetadata( - unusedUri: string | null, - metadataFn: (err: Error | null, metadata?: RequestMetadata) => void - ) { - messages.warn(messages.IAM_GET_REQUEST_METADATA_DEPRECATED); - metadataFn(null, this.getRequestHeaders()); - } - /** * Acquire the HTTP headers required to make an authenticated request. */ diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index 03cdd6d1..78f70a43 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -57,35 +57,6 @@ export class JWTAccess { this.keyId = keyId; } - /** - * Indicates whether the credential requires scopes to be created by calling - * createdScoped before use. - * @deprecated - * @return always false - */ - createScopedRequired(): boolean { - // JWT Header authentication does not use scopes. - messages.warn(messages.JWT_ACCESS_CREATE_SCOPED_DEPRECATED); - return false; - } - - /** - * Get a non-expired access token, after refreshing if necessary. - * - * @param authURI The URI being authorized. - * @param additionalClaims An object with a set of additional claims to - * include in the payload. - * @deprecated Please use `getRequestHeaders` instead. - * @returns An object that includes the authorization header. - */ - getRequestMetadata( - url: string, - additionalClaims?: Claims - ): RequestMetadataResponse { - messages.warn(messages.JWT_ACCESS_GET_REQUEST_METADATA_DEPRECATED); - return {headers: this.getRequestHeaders(url, additionalClaims)}; - } - /** * Get a non-expired access token, after refreshing if necessary. * diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index e4f173dc..f6a1ace9 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -174,17 +174,6 @@ export class JWT extends OAuth2Client implements IdTokenProvider { return gtoken.idToken; } - /** - * Indicates whether the credential requires scopes to be created by calling - * createScoped before use. - * @deprecated - * @return false if createScoped does not need to be called. - */ - createScopedRequired() { - messages.warn(messages.JWT_CREATE_SCOPED_DEPRECATED); - return !this.hasScopes(); - } - /** * Determine if there are currently scopes available. */ diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 21b16fb7..163d15a7 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -759,33 +759,6 @@ export class OAuth2Client extends AuthClient { } } - /** - * Obtain the set of headers required to authenticate a request. - * - * @deprecated Use getRequestHeaders instead. - * @param url the Uri being authorized - * @param callback the func described above - */ - getRequestMetadata(url?: string | null): Promise; - getRequestMetadata( - url: string | null, - callback: RequestMetadataCallback - ): void; - getRequestMetadata( - url: string | null, - callback?: RequestMetadataCallback - ): Promise | void { - messages.warn(messages.OAUTH_GET_REQUEST_METADATA_DEPRECATED); - if (callback) { - this.getRequestMetadataAsync(url).then( - r => callback(null, r.headers, r.res), - callback - ); - } else { - return this.getRequestMetadataAsync(); - } - } - /** * The main authentication interface. It takes an optional url which when * present is the endpoint being accessed, and returns a Promise which diff --git a/src/messages.ts b/src/messages.ts index e7add3eb..57598e4e 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -40,79 +40,3 @@ export interface Warning { message: string; warned?: boolean; } - -export const DEFAULT_PROJECT_ID_DEPRECATED = { - code: 'google-auth-library:DEP002', - type: WarningTypes.DEPRECATION, - message: [ - 'The `getDefaultProjectId` method has been deprecated, and will be removed', - 'in the 3.0 release of google-auth-library. Please use the `getProjectId`', - 'method instead.', - ].join(' '), -}; - -export const COMPUTE_CREATE_SCOPED_DEPRECATED = { - code: 'google-auth-library:DEP003', - type: WarningTypes.DEPRECATION, - message: [ - 'The `createScopedRequired` method on the `Compute` class has been deprecated,', - 'and will be removed in the 3.0 release of google-auth-library.', - ].join(' '), -}; - -export const JWT_CREATE_SCOPED_DEPRECATED = { - code: 'google-auth-library:DEP004', - type: WarningTypes.DEPRECATION, - message: [ - 'The `createScopedRequired` method on the `JWT` class has been deprecated,', - 'and will be removed in the 3.0 release of google-auth-library.', - ].join(' '), -}; - -export const IAM_CREATE_SCOPED_DEPRECATED = { - code: 'google-auth-library:DEP005', - type: WarningTypes.DEPRECATION, - message: [ - 'The `createScopedRequired` method on the `IAM` class has been deprecated,', - 'and will be removed in the 3.0 release of google-auth-library.', - ].join(' '), -}; - -export const JWT_ACCESS_CREATE_SCOPED_DEPRECATED = { - code: 'google-auth-library:DEP006', - type: WarningTypes.DEPRECATION, - message: [ - 'The `createScopedRequired` method on the `JWTAccess` class has been deprecated,', - 'and will be removed in the 3.0 release of google-auth-library.', - ].join(' '), -}; - -export const OAUTH_GET_REQUEST_METADATA_DEPRECATED = { - code: 'google-auth-library:DEP004', - type: WarningTypes.DEPRECATION, - message: [ - 'The `getRequestMetadata` method on the `OAuth2` class has been deprecated,', - 'and will be removed in the 3.0 release of google-auth-library. Please use', - 'the `getRequestHeaders` method instead.', - ].join(' '), -}; - -export const IAM_GET_REQUEST_METADATA_DEPRECATED = { - code: 'google-auth-library:DEP005', - type: WarningTypes.DEPRECATION, - message: [ - 'The `getRequestMetadata` method on the `IAM` class has been deprecated,', - 'and will be removed in the 3.0 release of google-auth-library. Please use', - 'the `getRequestHeaders` method instead.', - ].join(' '), -}; - -export const JWT_ACCESS_GET_REQUEST_METADATA_DEPRECATED = { - code: 'google-auth-library:DEP006', - type: WarningTypes.DEPRECATION, - message: [ - 'The `getRequestMetadata` method on the `JWTAccess` class has been deprecated,', - 'and will be removed in the 3.0 release of google-auth-library. Please use', - 'the `getRequestHeaders` method instead.', - ].join(' '), -}; diff --git a/test/test.compute.ts b/test/test.compute.ts index 974f1daf..365ee60e 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -150,19 +150,6 @@ describe('compute', () => { scope.done(); }); - it('should emit warning for createScopedRequired', () => { - let called = false; - sandbox.stub(process, 'emitWarning').callsFake(() => (called = true)); - // tslint:disable-next-line deprecation - compute.createScopedRequired(); - assert.strictEqual(called, true); - }); - - it('should return false for createScopedRequired', () => { - // tslint:disable-next-line deprecation - assert.strictEqual(false, compute.createScopedRequired()); - }); - it('should return a helpful message on request response.statusCode 403', async () => { const scope = mockToken(403); const expected = new RegExp( diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index c125046d..11bcd62f 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1407,36 +1407,6 @@ describe('googleauth', () => { assert.strictEqual(value, signedBlob); }); - it('should warn the user if using the getDefaultProjectId method', done => { - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - sandbox - .stub(process, 'emitWarning') - .callsFake((message, warningOrType) => { - assert.strictEqual( - message, - messages.DEFAULT_PROJECT_ID_DEPRECATED.message - ); - const warningType = - typeof warningOrType === 'string' - ? warningOrType - : // @types/node doesn't recognize the emitWarning syntax which - // tslint:disable-next-line no-any - (warningOrType as any).type; - assert.strictEqual(warningType, messages.WarningTypes.DEPRECATION); - done(); - }); - auth.getDefaultProjectId(); - }); - - it('should only emit warnings once', async () => { - // The warning was used above, so invoking it here should have no effect. - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - let count = 0; - sandbox.stub(process, 'emitWarning').callsFake(() => count++); - await auth.getDefaultProjectId(); - assert.strictEqual(count, 0); - }); - it('should pass options to the JWT constructor via constructor', async () => { const subject = 'science!'; const auth = new GoogleAuth({ diff --git a/test/test.iam.ts b/test/test.iam.ts index e382916a..d66fac7a 100644 --- a/test/test.iam.ts +++ b/test/test.iam.ts @@ -16,7 +16,6 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import * as sinon from 'sinon'; import {IAMAuth} from '../src'; -import * as messages from '../src/messages'; describe('iam', () => { const testSelector = 'a-test-selector'; @@ -38,20 +37,4 @@ describe('iam', () => { assert.strictEqual(creds!['x-goog-iam-authority-selector'], testSelector); assert.strictEqual(creds!['x-goog-iam-authorization-token'], testToken); }); - - it('should warn about deprecation of getRequestMetadata', done => { - const stub = sandbox.stub(messages, 'warn'); - // tslint:disable-next-line deprecation - client.getRequestMetadata(null, () => { - assert.strictEqual(stub.calledOnce, true); - done(); - }); - }); - - it('should emit warning for createScopedRequired', () => { - const stub = sandbox.stub(process, 'emitWarning'); - // tslint:disable-next-line deprecation - client.createScopedRequired(); - assert(stub.called); - }); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 3cfb3f5f..4b26a573 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -71,14 +71,6 @@ describe('jwt', () => { sandbox.restore(); }); - it('should emit warning for createScopedRequired', () => { - let called = false; - sandbox.stub(process, 'emitWarning').callsFake(() => (called = true)); - // tslint:disable-next-line deprecation - jwt.createScopedRequired(); - assert.strictEqual(called, true); - }); - it('should create a dummy refresh token string', () => { // It is important that the compute client is created with a refresh token // value filled in, or else the rest of the logic will not work. @@ -587,72 +579,6 @@ describe('jwt', () => { assert.notStrictEqual(jwt, clone); }); - it('createScopedRequired should return true when scopes is null', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - subject: 'bar@subjectaccount.com', - }); - // tslint:disable-next-line deprecation - assert.strictEqual(true, jwt.createScopedRequired()); - }); - - it('createScopedRequired should return true when scopes is an empty array', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: [], - subject: 'bar@subjectaccount.com', - }); - // tslint:disable-next-line deprecation - assert.strictEqual(true, jwt.createScopedRequired()); - }); - - it('createScopedRequired should return true when scopes is an empty string', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: '', - subject: 'bar@subjectaccount.com', - }); - // tslint:disable-next-line deprecation - assert.strictEqual(true, jwt.createScopedRequired()); - }); - - it('createScopedRequired should return false when scopes is a filled-in string', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: 'http://foo', - subject: 'bar@subjectaccount.com', - }); - // tslint:disable-next-line deprecation - assert.strictEqual(false, jwt.createScopedRequired()); - }); - - it('createScopedRequired should return false when scopes is a filled-in array', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - - // tslint:disable-next-line deprecation - assert.strictEqual(false, jwt.createScopedRequired()); - }); - - it('createScopedRequired should return false when scopes is not an array or a string, but can be used as a string', () => { - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - keyFile: '/path/to/key.pem', - scopes: '2', - subject: 'bar@subjectaccount.com', - }); - // tslint:disable-next-line deprecation - assert.strictEqual(false, jwt.createScopedRequired()); - }); - it('fromJson should error on null json', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index b76066cc..ca615b4b 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -38,21 +38,11 @@ describe('jwtaccess', () => { const email = 'foo@serviceaccount.com'; let client: JWTAccess; - let sandbox: sinon.SinonSandbox; + const sandbox = sinon.createSandbox(); beforeEach(() => { client = new JWTAccess(); - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); - - it('should emit warning for createScopedRequired', () => { - const stub = sandbox.stub(process, 'emitWarning'); - // tslint:disable-next-line deprecation - client.createScopedRequired(); - assert(stub.called); }); + afterEach(() => sandbox.restore()); it('getRequestHeaders should create a signed JWT token as the access token', () => { const client = new JWTAccess(email, keys.private); @@ -106,12 +96,6 @@ describe('jwtaccess', () => { } }); - it('createScopedRequired should return false', () => { - const client = new JWTAccess('foo@serviceaccount.com', null); - // tslint:disable-next-line deprecation - assert.strictEqual(false, client.createScopedRequired()); - }); - it('fromJson should error on null json', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. @@ -183,12 +167,4 @@ describe('jwtaccess', () => { assert.strictEqual(json.private_key, client.key); assert.strictEqual(json.client_email, client.email); }); - - it('should warn about deprecation of getRequestMetadata', () => { - const client = new JWTAccess(email, keys.private); - const stub = sandbox.stub(messages, 'warn'); - // tslint:disable-next-line deprecation - client.getRequestMetadata(testUri); - assert.strictEqual(stub.calledOnce, true); - }); }); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 2ddc95c5..f0b54947 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1376,14 +1376,6 @@ describe('oauth2', () => { assert.deepStrictEqual(info.scopes, tokenInfo.scope.split(' ')); }); - it('should warn about deprecation of getRequestMetadata', done => { - const stub = sandbox.stub(messages, 'warn'); - client.getRequestMetadata(null, () => { - assert.strictEqual(stub.calledOnce, true); - done(); - }); - }); - it('should throw if tries to refresh but no refresh token is available', async () => { client.setCredentials({ access_token: 'initial-access-token', From 1f4bf6128a0dcf22cfe1ec492b2192f513836cb2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 21 Mar 2020 21:54:07 +0100 Subject: [PATCH 112/662] fix(deps): update dependency gaxios to v3 (#917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [gaxios](https://togithub.com/googleapis/gaxios) | dependencies | major | [`^2.1.0` -> `^3.0.0`](https://renovatebot.com/diffs/npm/gaxios/2.3.2/3.0.0) | --- ### Release Notes
googleapis/gaxios ### [`v3.0.0`](https://togithub.com/googleapis/gaxios/blob/master/CHANGELOG.md#​300httpswwwgithubcomgoogleapisgaxioscomparev232v300-2020-03-19) [Compare Source](https://togithub.com/googleapis/gaxios/compare/v2.3.2...v3.0.0) ##### ⚠ BREAKING CHANGES - **deps:** TypeScript introduced breaking changes in generated code in 3.7.x - drop Node 8 from engines field ([#​254](https://togithub.com/googleapis/gaxios/issues/254)) ##### Features - drop Node 8 from engines field ([#​254](https://www.github.com/googleapis/gaxios/issues/254)) ([8c9fff7](https://www.github.com/googleapis/gaxios/commit/8c9fff7f92f70f029292c906c62d194c1d58827d)) - **deps:** updates to latest TypeScript ([#​253](https://www.github.com/googleapis/gaxios/issues/253)) ([054267b](https://www.github.com/googleapis/gaxios/commit/054267bf12e1801c134e3b5cae92dcc5ea041fab)) ##### [2.3.2](https://www.github.com/googleapis/gaxios/compare/v2.3.1...v2.3.2) (2020-02-28) ##### Bug Fixes - update github repo in package ([#​239](https://www.github.com/googleapis/gaxios/issues/239)) ([7e750cb](https://www.github.com/googleapis/gaxios/commit/7e750cbaaa59812817d725c74fb9d364c4b71096)) ##### [2.3.1](https://www.github.com/googleapis/gaxios/compare/v2.3.0...v2.3.1) (2020-02-13) ##### Bug Fixes - **deps:** update dependency https-proxy-agent to v5 ([#​233](https://www.github.com/googleapis/gaxios/issues/233)) ([56de0a8](https://www.github.com/googleapis/gaxios/commit/56de0a824a2f9622e3e4d4bdd41adccd812a30b4))
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 071ddfca..23f07772 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", + "gaxios": "^3.0.0", "gcp-metadata": "^4.0.0", "gtoken": "^4.1.0", "jws": "^4.0.0", From ef8b18a21c3c3348150aa1e17abeb2dcb2a30573 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 25 Mar 2020 00:04:27 -0700 Subject: [PATCH 113/662] chore: update github actions configuration (#922) This PR was generated using Autosynth. :rainbow:
Log from Synthtool ``` 2020-03-22 04:10:17,761 synthtool > Executing /tmpfs/src/git/autosynth/working_repo/synth.py. .eslintignore .eslintrc.yml .github/ISSUE_TEMPLATE/bug_report.md .github/ISSUE_TEMPLATE/feature_request.md .github/ISSUE_TEMPLATE/support_request.md .github/PULL_REQUEST_TEMPLATE.md .github/publish.yml .github/release-please.yml .github/workflows/ci.yaml .kokoro/common.cfg .kokoro/continuous/node10/common.cfg .kokoro/continuous/node10/docs.cfg .kokoro/continuous/node10/lint.cfg .kokoro/continuous/node10/samples-test.cfg .kokoro/continuous/node10/system-test.cfg .kokoro/continuous/node10/test.cfg .kokoro/continuous/node12/common.cfg .kokoro/continuous/node12/test.cfg .kokoro/continuous/node8/common.cfg .kokoro/continuous/node8/test.cfg .kokoro/docs.sh .kokoro/lint.sh .kokoro/presubmit/node10/common.cfg .kokoro/presubmit/node10/docs.cfg .kokoro/presubmit/node10/lint.cfg .kokoro/presubmit/node10/samples-test.cfg .kokoro/presubmit/node10/system-test.cfg .kokoro/presubmit/node10/test.cfg .kokoro/presubmit/node12/common.cfg .kokoro/presubmit/node12/test.cfg .kokoro/presubmit/node8/common.cfg .kokoro/presubmit/node8/test.cfg .kokoro/presubmit/windows/common.cfg .kokoro/presubmit/windows/test.cfg .kokoro/publish.sh .kokoro/release/docs.cfg .kokoro/release/docs.sh .kokoro/release/publish.cfg .kokoro/samples-test.sh .kokoro/system-test.sh .kokoro/test.bat .kokoro/test.sh .kokoro/trampoline.sh .mocharc.js .nycrc .prettierignore .prettierrc CODE_OF_CONDUCT.md CONTRIBUTING.md LICENSE README.md codecov.yaml renovate.json samples/README.md 2020-03-22 04:10:18,576 synthtool > Wrote metadata to synth.metadata. ```
--- .github/workflows/ci.yaml | 8 ++++---- synth.metadata | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c5cbc554..92394b1e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [8, 10, 12, 13] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node }} @@ -30,7 +30,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 12 @@ -39,7 +39,7 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 12 @@ -48,7 +48,7 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 13 diff --git a/synth.metadata b/synth.metadata index 9a038f82..fc4b8607 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,11 +1,11 @@ { - "updateTime": "2020-03-03T12:10:12.656651Z", + "updateTime": "2020-03-22T11:10:18.576330Z", "sources": [ { - "template": { - "name": "node_library", - "origin": "synthtool.gcp", - "version": "2020.2.4" + "git": { + "name": "synthtool", + "remote": "https://github.com/googleapis/synthtool.git", + "sha": "7e98e1609c91082f4eeb63b530c6468aefd18cfd" } } ] From d89c59a316e9ca5b8c351128ee3e2d91e9729d5c Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 25 Mar 2020 16:20:59 -0700 Subject: [PATCH 114/662] feat!: require node 10 in engines field (#926) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23f07772..2cc0585b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { - "node": ">=8.10.0" + "node": ">=10" }, "main": "./build/src/index.js", "types": "./build/src/index.d.ts", From e11e18cb33eb60a666980d061c54bb8891cdd242 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Thu, 26 Mar 2020 12:00:02 -0700 Subject: [PATCH 115/662] build!: update to latest gts and TypeScript (#927) BREAKING CHANGE: typescript@3.7.x introduced some breaking changes in generated code. --- .eslintrc.json | 3 + .eslintrc.yml | 15 ---- .github/workflows/ci.yaml | 2 +- .prettierignore | 8 ++- .prettierrc | 8 --- .prettierrc.js | 17 +++++ browser-test/test.crypto.ts | 1 + browser-test/test.oauth2.ts | 8 ++- karma.conf.js | 2 +- package.json | 13 +--- src/auth/googleauth.ts | 4 ++ src/auth/oauth2client.ts | 2 +- src/crypto/browser/crypto.ts | 7 ++ src/crypto/crypto.ts | 1 + src/crypto/node/crypto.ts | 5 +- src/transporters.ts | 3 +- system-test/test.kitchen.ts | 5 +- test/test.compute.ts | 13 ++-- test/test.googleauth.ts | 76 ++++++++------------ test/test.index.ts | 2 + test/test.jwt.ts | 17 ++--- test/test.jwtaccess.ts | 1 + test/test.oauth2.ts | 133 ++++++++++++----------------------- test/test.transporters.ts | 19 ++--- tsconfig.json | 2 +- 25 files changed, 152 insertions(+), 215 deletions(-) create mode 100644 .eslintrc.json delete mode 100644 .eslintrc.yml delete mode 100644 .prettierrc create mode 100644 .prettierrc.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..78215349 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts" +} diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 73eeec27..00000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- -extends: - - 'eslint:recommended' - - 'plugin:node/recommended' - - prettier -plugins: - - node - - prettier -rules: - prettier/prettier: error - block-scoped-var: error - eqeqeq: error - no-warning-comments: warn - no-var: error - prefer-const: error diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 92394b1e..7138a79a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [8, 10, 12, 13] + node: [10, 12, 13] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 diff --git a/.prettierignore b/.prettierignore index f6fac98b..a4ac7b37 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,5 @@ -node_modules/* -samples/node_modules/* -src/**/doc/* +**/node_modules +**/.coverage +build/ +docs/ +protos/ diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index df6eac07..00000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ ---- -bracketSpacing: false -printWidth: 80 -semi: true -singleQuote: true -tabWidth: 2 -trailingComma: es5 -useTabs: false diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..08cba377 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,17 @@ +// Copyright 2020 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. + +module.exports = { + ...require('gts/.prettierrc.json') +} diff --git a/browser-test/test.crypto.ts b/browser-test/test.crypto.ts index c152c5eb..aaa13b3f 100644 --- a/browser-test/test.crypto.ts +++ b/browser-test/test.crypto.ts @@ -17,6 +17,7 @@ import {assert} from 'chai'; import {createCrypto} from '../src/crypto/crypto'; import {BrowserCrypto} from '../src/crypto/browser/crypto'; import {privateKey, publicKey} from './fixtures/keys'; +import {describe, it} from 'mocha'; // Not all browsers support `TextEncoder`. The following `require` will // provide a fast UTF8-only replacement for those browsers that don't support diff --git a/browser-test/test.oauth2.ts b/browser-test/test.oauth2.ts index a87429a1..d94bbabd 100644 --- a/browser-test/test.oauth2.ts +++ b/browser-test/test.oauth2.ts @@ -16,6 +16,7 @@ import * as base64js from 'base64-js'; import {assert} from 'chai'; import * as sinon from 'sinon'; import {privateKey, publicKey} from './fixtures/keys'; +import {it, describe, beforeEach} from 'mocha'; // Not all browsers support `TextEncoder`. The following `require` will // provide a fast UTF8-only replacement for those browsers that don't support @@ -127,7 +128,7 @@ describe('Browser OAuth2 tests', () => { it('should generate a valid code verifier and resulting challenge', async () => { const codes = await client.generateCodeVerifierAsync(); - assert.match(codes.codeVerifier, /^[a-zA-Z0-9\-\.~_]{128}$/); + assert.match(codes.codeVerifier, /^[a-zA-Z0-9-.~_]{128}$/); }); it('should include code challenge and method in the url', async () => { @@ -166,13 +167,16 @@ describe('Browser OAuth2 tests', () => { '}'; const envelope = JSON.stringify({kid: 'keyid', alg: 'RS256'}); let data = + // eslint-disable-next-line node/no-unsupported-features/node-builtins base64js.fromByteArray(new TextEncoder().encode(envelope)) + '.' + + // eslint-disable-next-line node/no-unsupported-features/node-builtins base64js.fromByteArray(new TextEncoder().encode(idToken)); const algo = { name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'}, }; + // eslint-disable-next-line no-undef const cryptoKey = await window.crypto.subtle.importKey( 'jwk', privateKey, @@ -180,9 +184,11 @@ describe('Browser OAuth2 tests', () => { true, ['sign'] ); + // eslint-disable-next-line no-undef const signature = await window.crypto.subtle.sign( algo, cryptoKey, + // eslint-disable-next-line node/no-unsupported-features/node-builtins new TextEncoder().encode(data) ); data += '.' + base64js.fromByteArray(new Uint8Array(signature)); diff --git a/karma.conf.js b/karma.conf.js index 5b812100..54433e4e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -19,7 +19,7 @@ const isDocker = require('is-docker')(); const webpackConfig = require('./webpack-tests.config.js'); process.env.CHROME_BIN = require('puppeteer').executablePath(); -module.exports = function(config) { +module.exports = function (config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', diff --git a/package.json b/package.json index 2cc0585b..cfe20ee5 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,9 @@ "c8": "^7.0.0", "chai": "^4.2.0", "codecov": "^3.0.2", - "eslint": "^6.0.0", - "eslint-config-prettier": "^6.0.0", - "eslint-plugin-node": "^11.0.0", - "eslint-plugin-prettier": "^3.0.0", "execa": "^4.0.0", - "gts": "^1.1.2", + "gts": "^2.0.0-alpha.8", "is-docker": "^2.0.0", - "js-green-licenses": "^1.0.0", "karma": "^4.0.0", "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^2.0.0", @@ -66,12 +61,11 @@ "ncp": "^2.0.0", "nock": "^12.0.0", "null-loader": "^3.0.0", - "prettier": "^1.13.4", "puppeteer": "^2.0.0", "sinon": "^9.0.0", "tmp": "^0.1.0", "ts-loader": "^6.0.0", - "typescript": "3.6.4", + "typescript": "^3.8.3", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" }, @@ -83,11 +77,10 @@ "test": "c8 mocha build/test", "clean": "gts clean", "prepare": "npm run compile", - "lint": "gts check && eslint '**/*.js' && jsgl --local .", + "lint": "gts check", "compile": "tsc -p .", "fix": "gts fix && eslint --fix '**/*.js'", "pretest": "npm run compile", - "license-check": "jsgl --local .", "docs": "compodoc src/", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "system-test": "mocha build/system-test --timeout 60000", diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 11fce60c..62d7b8e6 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -45,6 +45,7 @@ export interface CredentialCallback { (err: Error | null, result?: UserRefreshClient | JWT): void; } +// eslint-disable-next-line @typescript-eslint/no-empty-interface interface DeprecatedGetClientOptions {} export interface ADCCallback { @@ -164,7 +165,10 @@ export class GoogleAuth { // - 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 = diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 163d15a7..588ac804 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -1216,7 +1216,7 @@ export class OAuth2Client extends AuthClient { throw new Error("Can't parse token payload: " + segments[1]); } - if (!certs.hasOwnProperty(envelope.kid)) { + if (!Object.prototype.hasOwnProperty.call(certs, envelope.kid)) { // If this is not present, then there's no reason to attempt verification throw new Error('No pem found for envelope: ' + JSON.stringify(envelope)); } diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index f84191b6..5e1665f0 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -11,6 +11,7 @@ // 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. +/* global window */ // This file implements crypto functions we need using in-browser // SubtleCrypto interface `window.crypto.subtle`. @@ -20,6 +21,7 @@ import * as base64js from 'base64-js'; // Not all browsers support `TextEncoder`. The following `require` will // provide a fast UTF8-only replacement for those browsers that don't support // text encoding natively. +// eslint-disable-next-line node/no-unsupported-features/node-builtins if (typeof process === 'undefined' && typeof TextEncoder === 'undefined') { require('fast-text-encoding'); } @@ -45,6 +47,7 @@ export class BrowserCrypto implements Crypto { // To calculate SHA256 digest using SubtleCrypto, we first // need to convert an input string to an ArrayBuffer: + // eslint-disable-next-line node/no-unsupported-features/node-builtins const inputBuffer = new TextEncoder().encode(str); // Result is ArrayBuffer as well. @@ -79,6 +82,7 @@ export class BrowserCrypto implements Crypto { name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'}, }; + // eslint-disable-next-line node/no-unsupported-features/node-builtins const dataArray = new TextEncoder().encode(data); const signatureArray = base64js.toByteArray( BrowserCrypto.padBase64(signature) @@ -107,6 +111,7 @@ export class BrowserCrypto implements Crypto { name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'}, }; + // eslint-disable-next-line node/no-unsupported-features/node-builtins const dataArray = new TextEncoder().encode(data); const cryptoKey = await window.crypto.subtle.importKey( 'jwk', @@ -124,11 +129,13 @@ export class BrowserCrypto implements Crypto { decodeBase64StringUtf8(base64: string): string { const uint8array = base64js.toByteArray(BrowserCrypto.padBase64(base64)); + // eslint-disable-next-line node/no-unsupported-features/node-builtins const result = new TextDecoder().decode(uint8array); return result; } encodeBase64StringUtf8(text: string): string { + // eslint-disable-next-line node/no-unsupported-features/node-builtins const uint8array = new TextEncoder().encode(text); const result = base64js.fromByteArray(uint8array); return result; diff --git a/src/crypto/crypto.ts b/src/crypto/crypto.ts index 45ca5c9b..27ce5a9d 100644 --- a/src/crypto/crypto.ts +++ b/src/crypto/crypto.ts @@ -11,6 +11,7 @@ // 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. +/* global window */ import {BrowserCrypto} from './browser/crypto'; import {NodeCrypto} from './node/crypto'; diff --git a/src/crypto/node/crypto.ts b/src/crypto/node/crypto.ts index 215ea7a4..58b60671 100644 --- a/src/crypto/node/crypto.ts +++ b/src/crypto/node/crypto.ts @@ -17,10 +17,7 @@ import {Crypto} from '../crypto'; export class NodeCrypto implements Crypto { async sha256DigestBase64(str: string): Promise { - return crypto - .createHash('sha256') - .update(str) - .digest('base64'); + return crypto.createHash('sha256').update(str).digest('base64'); } randomBytesBase64(count: number): string { diff --git a/src/transporters.ts b/src/transporters.ts index e9feb5be..3b3fded6 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -21,8 +21,9 @@ import { } from 'gaxios'; import {validate} from './options'; -// tslint:disable-next-line no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../package.json'); + const PRODUCT_NAME = 'google-api-nodejs-client'; export interface Transporter { diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 30933d24..630f9b82 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {describe, it, before, after} from 'mocha'; import * as execa from 'execa'; import * as fs from 'fs'; import * as mv from 'mv'; @@ -27,6 +27,7 @@ const ncpp = promisify(ncp); const keep = !!process.env.GALN_KEEP_TEMPDIRS; const stagingDir = tmp.dirSync({keep, unsafeCleanup: true}); const stagingPath = stagingDir.name; +// eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../package.json'); describe('pack and install', () => { @@ -34,7 +35,7 @@ describe('pack and install', () => { * Create a staging directory with temp fixtures used to test on a fresh * application. */ - before('should be able to use the d.ts', async function() { + before('should be able to use the d.ts', async function () { this.timeout(40000); console.log(`${__filename} staging area: ${stagingPath}`); await execa('npm', ['pack'], {stdio: 'inherit'}); diff --git a/test/test.compute.ts b/test/test.compute.ts index 365ee60e..05d476ec 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -14,7 +14,6 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; -const assertRejects = require('assert-rejects'); import {BASE_PATH, HEADERS, HOST_ADDRESS} from 'gcp-metadata'; import * as nock from 'nock'; import * as sinon from 'sinon'; @@ -37,9 +36,7 @@ describe('compute', () => { } function mockExample() { - return nock(url) - .get('/') - .reply(200); + return nock(url).get('/').reply(200); } // set up compute client. @@ -158,7 +155,7 @@ describe('compute', () => { 'Compute Engine instance does not have the correct permission scopes specified. ' + 'Could not refresh access token.' ); - await assertRejects(compute.request({url}), expected); + await assert.rejects(compute.request({url}), expected); scope.done(); }); @@ -169,7 +166,7 @@ describe('compute', () => { 'token for the Compute Engine built-in service account. This may be because the ' + 'Compute Engine instance does not have any permission scopes specified.' ); - await assertRejects(compute.request({url}), expected); + await assert.rejects(compute.request({url}), expected); scope.done(); }); @@ -188,7 +185,7 @@ describe('compute', () => { 'Compute Engine instance does not have the correct permission scopes specified. ' + 'Could not refresh access token.' ); - await assertRejects(compute.request({}), expected); + await assert.rejects(compute.request({}), expected); scope.done(); }); @@ -210,7 +207,7 @@ describe('compute', () => { 'refresh access token.' ); - await assertRejects(compute.request({}), expected); + await assert.rejects(compute.request({}), expected); scope.done(); }); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 11bcd62f..a813cff3 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -14,7 +14,6 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; -const assertRejects = require('assert-rejects'); import * as child_process from 'child_process'; import * as crypto from 'crypto'; import * as fs from 'fs'; @@ -54,8 +53,11 @@ describe('googleauth', () => { STUB_PROJECT, ].join('/'); + // eslint-disable-next-line @typescript-eslint/no-var-requires const privateJSON = require('../../test/fixtures/private.json'); + // eslint-disable-next-line @typescript-eslint/no-var-requires const private2JSON = require('../../test/fixtures/private2.json'); + // eslint-disable-next-line @typescript-eslint/no-var-requires const refreshJSON = require('../../test/fixtures/refresh.json'); const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); const wellKnownPathWindows = path.join( @@ -165,9 +167,7 @@ describe('googleauth', () => { } function nockIsGCE() { - const primary = nock(host) - .get(instancePath) - .reply(200, {}, HEADERS); + const primary = nock(host).get(instancePath).reply(200, {}, HEADERS); const secondary = nock(SECONDARY_HOST_ADDRESS) .get(instancePath) .reply(200, {}, HEADERS); @@ -204,9 +204,7 @@ describe('googleauth', () => { } function nock500GCE() { - const primary = nock(host) - .get(instancePath) - .reply(500, {}, HEADERS); + const primary = nock(host).get(instancePath).reply(500, {}, HEADERS); const secondary = nock(SECONDARY_HOST_ADDRESS) .get(instancePath) .reply(500, {}, HEADERS); @@ -224,9 +222,7 @@ describe('googleauth', () => { } function nock404GCE() { - const primary = nock(host) - .get(instancePath) - .reply(404); + const primary = nock(host).get(instancePath).reply(404); const secondary = nock(SECONDARY_HOST_ADDRESS) .get(instancePath) .reply(404); @@ -315,7 +311,7 @@ describe('googleauth', () => { it('should make a request with the api key', async () => { const scope = nock(BASE_URL) .post(ENDPOINT) - .reply(function(uri) { + .reply(function (uri) { assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); return [200, RESPONSE_BODY]; }); @@ -340,7 +336,7 @@ describe('googleauth', () => { const scope = nock(BASE_URL) .post(ENDPOINT) .query({test: OTHER_QS_PARAM.test}) - .reply(function(uri) { + .reply(function (uri) { assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); assert(uri.indexOf('test=' + OTHER_QS_PARAM.test) > -1); return [200, RESPONSE_BODY]; @@ -501,7 +497,7 @@ describe('googleauth', () => { }); it('getApplicationCredentialsFromFilePath should error on invalid symlink', async () => { - await assertRejects( + await assert.rejects( auth._getApplicationCredentialsFromFilePath('./test/fixtures/badlink') ); }); @@ -511,7 +507,7 @@ describe('googleauth', () => { // git does not create symlinks on Windows return; } - await assertRejects( + await assert.rejects( auth._getApplicationCredentialsFromFilePath('./test/fixtures/emptylink') ); }); @@ -561,13 +557,13 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should error on directory', async () => { // Make sure that the following path actually does point to a directory. const directory = './test/fixtures'; - await assertRejects( + await assert.rejects( auth._getApplicationCredentialsFromFilePath(directory) ); }); it('getApplicationCredentialsFromFilePath should handle errors thrown from createReadStream', async () => { - await assertRejects( + await assert.rejects( auth._getApplicationCredentialsFromFilePath('./does/not/exist.json'), /ENOENT: no such file or directory/ ); @@ -575,7 +571,7 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should handle errors thrown from fromStream', async () => { sandbox.stub(auth, 'fromStream').throws('🤮'); - await assertRejects( + await assert.rejects( auth._getApplicationCredentialsFromFilePath( './test/fixtures/private.json' ), @@ -586,7 +582,7 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should handle errors passed from fromStream', async () => { // Set up a mock to return an error from the fromStream method. sandbox.stub(auth, 'fromStream').throws('🤮'); - await assertRejects( + await assert.rejects( auth._getApplicationCredentialsFromFilePath( './test/fixtures/private.json' ), @@ -726,7 +722,7 @@ describe('googleauth', () => { sandbox .stub(auth, '_getApplicationCredentialsFromFilePath') .rejects('🤮'); - await assertRejects( + await assert.rejects( auth._tryGetApplicationCredentialsFromWellKnownFile(), /🤮/ ); @@ -737,7 +733,7 @@ describe('googleauth', () => { sandbox .stub(auth, '_getApplicationCredentialsFromFilePath') .rejects('🤮'); - await assertRejects( + await assert.rejects( auth._tryGetApplicationCredentialsFromWellKnownFile(), /🤮/ ); @@ -994,7 +990,7 @@ describe('googleauth', () => { // * Running on GCE is set to true. mockWindows(); sandbox.stub(auth, '_checkIsGCE').rejects('🤮'); - await assertRejects( + await assert.rejects( auth.getApplicationDefault(), /Unexpected error determining execution environment/ ); @@ -1052,7 +1048,7 @@ describe('googleauth', () => { it('_checkIsGCE should throw on unexpected errors', async () => { assert.notStrictEqual(true, auth.isGCE); const scope = nock500GCE(); - await assertRejects(auth._checkIsGCE()); + await assert.rejects(auth._checkIsGCE()); assert.strictEqual(undefined, auth.isGCE); scope.done(); }); @@ -1096,9 +1092,7 @@ describe('googleauth', () => { const scopes = [ nockIsGCE(), createGetProjectIdNock(), - nock(host) - .get(svcAccountPath) - .reply(200, response, HEADERS), + nock(host).get(svcAccountPath).reply(200, response, HEADERS), ]; await auth._checkIsGCE(); assert.strictEqual(true, auth.isGCE); @@ -1116,13 +1110,11 @@ describe('googleauth', () => { const scopes = [ nockIsGCE(), createGetProjectIdNock(), - nock(HOST_ADDRESS) - .get(svcAccountPath) - .reply(404), + nock(HOST_ADDRESS).get(svcAccountPath).reply(404), ]; await auth._checkIsGCE(); assert.strictEqual(true, auth.isGCE); - await assertRejects( + await assert.rejects( auth.getCredentials(), /Unsuccessful response status code. Request failed with status code 404/ ); @@ -1133,13 +1125,11 @@ describe('googleauth', () => { const scopes = [ nockIsGCE(), createGetProjectIdNock(), - nock(HOST_ADDRESS) - .get(svcAccountPath) - .reply(200, {}), + nock(HOST_ADDRESS).get(svcAccountPath).reply(200, {}), ]; await auth._checkIsGCE(); assert.strictEqual(true, auth.isGCE); - await assertRejects( + await assert.rejects( auth.getCredentials(), /Invalid response from metadata service: incorrect Metadata-Flavor header./ ); @@ -1202,7 +1192,7 @@ describe('googleauth', () => { // Set up a mock to return a null path string const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); assert.strictEqual(null, client); - await assertRejects(auth.getCredentials()); + await assert.rejects(auth.getCredentials()); }); it('should use jsonContent if available', async () => { @@ -1224,7 +1214,7 @@ describe('googleauth', () => { it('should error when invalid keyFilename passed to getClient', async () => { const auth = new GoogleAuth({keyFilename: './funky/fresh.json'}); - await assertRejects( + await assert.rejects( auth.getClient(), /ENOENT: no such file or directory/ ); @@ -1361,11 +1351,7 @@ describe('googleauth', () => { const {auth, scopes} = mockGCE(); scopes.push(createGetProjectIdNock()); const data = {breakfast: 'coffee'}; - scopes.push( - nock(url) - .get('/') - .reply(200, data) - ); + scopes.push(nock(url).get('/').reply(200, data)); const res = await auth.request({url}); scopes.forEach(s => s.done()); assert.deepStrictEqual(res.data, data); @@ -1395,9 +1381,7 @@ describe('googleauth', () => { const signedBlob = 'erutangis'; const data = 'abc123'; scopes.push( - nock(iamUri) - .post(iamPath) - .reply(200, {signedBlob}), + nock(iamUri).post(iamPath).reply(200, {signedBlob}), nock(host) .get(svcAccountPath) .reply(200, {default: {email, private_key: privateKey}}, HEADERS) @@ -1420,7 +1404,7 @@ describe('googleauth', () => { it('should throw if getProjectId cannot find a projectId', async () => { // tslint:disable-next-line no-any sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); - await assertRejects( + await assert.rejects( auth.getProjectId(), /Unable to detect a Project Id in the current environment/ ); @@ -1428,7 +1412,7 @@ describe('googleauth', () => { it('should throw if options are passed to getClient()', async () => { const auth = new GoogleAuth(); - await assertRejects( + await assert.rejects( auth.getClient({hello: 'world'}), /Passing options to getClient is forbidden in v5.0.0/ ); @@ -1475,7 +1459,7 @@ describe('googleauth', () => { assert(client instanceof UserRefreshClient); const apiReq = nock(BASE_URL) .post(ENDPOINT) - .reply(function(uri) { + .reply(function (uri) { assert.strictEqual( this.req.headers['x-goog-user-project'][0], 'my-quota-project' diff --git a/test/test.index.ts b/test/test.index.ts index 935f21c1..a68e00f3 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -17,11 +17,13 @@ import * as gal from '../src'; describe('index', () => { it('should publicly export GoogleAuth', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires const cjs = require('../src/'); assert.strictEqual(cjs.GoogleAuth, gal.GoogleAuth); }); it('should publicly export DefaultTransporter', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires const cjs = require('../src'); assert.strictEqual(cjs.DefaultTransporter, gal.DefaultTransporter); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 4b26a573..ac643dd4 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -23,6 +23,7 @@ import {GoogleAuth, JWT} from '../src'; import {CredentialRequest, JWTInput} from '../src/auth/credentials'; describe('jwt', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires const keypair = require('keypair'); const PEM_PATH = './test/fixtures/private.pem'; const PEM_CONTENTS = fs.readFileSync(PEM_PATH, 'utf8'); @@ -288,10 +289,7 @@ describe('jwt', () => { // endpoint. This makes sure that refreshToken is called only once. const scopes = [ createGTokenMock({access_token: 'abc123'}), - nock('http://example.com') - .get('/') - .thrice() - .reply(200), + nock('http://example.com').get('/').thrice().reply(200), ]; const jwt = new JWT({ email: 'foo@serviceaccount.com', @@ -316,10 +314,7 @@ describe('jwt', () => { const scopes = [ createGTokenMock({access_token: 'abc123'}), createGTokenMock({access_token: 'abc123'}), - nock('http://example.com') - .get('/') - .twice() - .reply(200), + nock('http://example.com').get('/').twice().reply(200), ]; const jwt = new JWT({ email: 'foo@serviceaccount.com', @@ -404,10 +399,7 @@ describe('jwt', () => { }); it('should refresh token if the server returns 403', done => { - nock('http://example.com') - .get('/access') - .twice() - .reply(403); + nock('http://example.com').get('/access').twice().reply(403); const jwt = new JWT({ email: 'foo@serviceaccount.com', keyFile: PEM_PATH, @@ -745,6 +737,7 @@ describe('jwt', () => { it('getRequestHeaders populates x-goog-user-project for JWT client', async () => { const auth = new GoogleAuth({ credentials: Object.assign( + // eslint-disable-next-line @typescript-eslint/no-var-requires require('../../test/fixtures/service-account-with-quota.json'), { private_key: keypair(512 /* bitsize of private key */).private, diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index ca615b4b..afe57b2a 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -21,6 +21,7 @@ import * as sinon from 'sinon'; import {JWTAccess} from '../src'; import * as messages from '../src/messages'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const keypair = require('keypair'); describe('jwtaccess', () => { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index f0b54947..718f91c8 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -14,7 +14,6 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; -const assertRejects = require('assert-rejects'); import * as crypto from 'crypto'; import * as formatEcdsa from 'ecdsa-sig-formatter'; import * as fs from 'fs'; @@ -86,16 +85,12 @@ describe('oauth2', () => { }); const generated = oauth2client.generateAuthUrl(opts); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.response_type, 'code token'); - assert.strictEqual(query.access_type, ACCESS_TYPE); - assert.strictEqual(query.scope, SCOPE); - assert.strictEqual(query.client_id, CLIENT_ID); - assert.strictEqual(query.redirect_uri, REDIRECT_URI); + const query = new URL(generated).searchParams; + assert.strictEqual(query.get('response_type'), 'code token'); + assert.strictEqual(query.get('access_type'), ACCESS_TYPE); + assert.strictEqual(query.get('scope'), SCOPE); + assert.strictEqual(query.get('client_id'), CLIENT_ID); + assert.strictEqual(query.get('redirect_uri'), REDIRECT_URI); done(); }); @@ -115,7 +110,7 @@ describe('oauth2', () => { const codes = await client.generateCodeVerifierAsync(); // ensure the code_verifier matches all requirements assert.strictEqual(codes.codeVerifier.length, 128); - const match = codes.codeVerifier.match(/[a-zA-Z0-9\-\.~_]*/); + const match = codes.codeVerifier.match(/[a-zA-Z0-9-.~_]*/); assert(match); if (!match) return; assert(match.length > 0 && match[0] === codes.codeVerifier); @@ -127,13 +122,12 @@ describe('oauth2', () => { code_challenge: codes.codeChallenge, code_challenge_method: CodeChallengeMethod.S256, }); - const parsed = url.parse(authUrl); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const props = qs.parse(parsed.query); - assert.strictEqual(props.code_challenge, codes.codeChallenge); - assert.strictEqual(props.code_challenge_method, CodeChallengeMethod.S256); + const props = new URL(authUrl).searchParams; + assert.strictEqual(props.get('code_challenge'), codes.codeChallenge); + assert.strictEqual( + props.get('code_challenge_method'), + CodeChallengeMethod.S256 + ); }); it('should verifyIdToken properly', async () => { @@ -208,22 +202,14 @@ describe('oauth2', () => { response_type: 'code token', }; const generated = client.generateAuthUrl(opts); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.scope, SCOPE_ARRAY.join(' ')); + const parsed = new URL(generated).searchParams; + assert.strictEqual(parsed.get('scope'), SCOPE_ARRAY.join(' ')); }); it('should set response_type param to code if none is given while generating the consent page url', () => { const generated = client.generateAuthUrl(); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.response_type, 'code'); + const parsed = new URL(generated).searchParams; + assert.strictEqual(parsed.get('response_type'), 'code'); }); it('should verify a valid certificate against a jwt', async () => { @@ -291,7 +277,7 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -331,7 +317,7 @@ describe('oauth2', () => { const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; const validAudiences = ['testaudience', 'extra-audience']; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -364,7 +350,7 @@ describe('oauth2', () => { const signature = signer.sign(privateKey, 'base64'); // Originally: data += '.'+signature; data += signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -402,13 +388,13 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, 'testaudience' ), - /Can\'t parse token envelope/ + /Can't parse token envelope/ ); }); @@ -440,13 +426,13 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, 'testaudience' ), - /Can\'t parse token payload/ + /Can't parse token payload/ ); }); @@ -476,7 +462,7 @@ describe('oauth2', () => { Buffer.from(idToken).toString('base64') + '.' + 'broken-signature'; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -509,7 +495,7 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -544,7 +530,7 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -582,7 +568,7 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -660,7 +646,7 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -701,7 +687,7 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -739,7 +725,7 @@ describe('oauth2', () => { signer.update(data); const signature = signer.sign(privateKey, 'base64'); data += '.' + signature; - return assertRejects( + return assert.rejects( client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, @@ -883,42 +869,26 @@ describe('oauth2', () => { it('should set redirect_uri if not provided in options', () => { const generated = client.generateAuthUrl({}); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.redirect_uri, REDIRECT_URI); + const parsed = new URL(generated).searchParams; + assert.strictEqual(parsed.get('redirect_uri'), REDIRECT_URI); }); it('should set client_id if not provided in options', () => { const generated = client.generateAuthUrl({}); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.client_id, CLIENT_ID); + const parsed = new URL(generated).searchParams; + assert.strictEqual(parsed.get('client_id'), CLIENT_ID); }); it('should override redirect_uri if provided in options', () => { const generated = client.generateAuthUrl({redirect_uri: 'overridden'}); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.redirect_uri, 'overridden'); + const parsed = new URL(generated).searchParams; + assert.strictEqual(parsed.get('redirect_uri'), 'overridden'); }); it('should override client_id if provided in options', () => { const generated = client.generateAuthUrl({client_id: 'client_override'}); - const parsed = url.parse(generated); - if (typeof parsed.query !== 'string') { - throw new Error('Unable to parse querystring'); - } - const query = qs.parse(parsed.query); - assert.strictEqual(query.client_id, 'client_override'); + const parsed = new URL(generated).searchParams; + assert.strictEqual(parsed.get('client_id'), 'client_override'); }); it('should return error in callback on request', done => { @@ -955,9 +925,7 @@ describe('oauth2', () => { reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, }) .reply(200, {access_token: 'abc123', expires_in: 1}), - nock('http://example.com') - .get('/') - .reply(200), + nock('http://example.com').get('/').reply(200), ]; } @@ -991,10 +959,7 @@ describe('oauth2', () => { reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, }) .reply(200, {access_token: 'abc123', expires_in: 1}), - nock('http://example.com') - .get('/') - .thrice() - .reply(200), + nock('http://example.com').get('/').thrice().reply(200), ]; client.credentials = {refresh_token: 'refresh-token-placeholder'}; await Promise.all([ @@ -1017,10 +982,7 @@ describe('oauth2', () => { }) .twice() .reply(200, {access_token: 'abc123', expires_in: 100000}), - nock('http://example.com') - .get('/') - .twice() - .reply(200), + nock('http://example.com').get('/').twice().reply(200), ]; client.credentials = {refresh_token: 'refresh-token-placeholder'}; await client.request({url: 'http://example.com'}); @@ -1043,13 +1005,12 @@ describe('oauth2', () => { reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, }) .reply(200, {access_token: 'abc123', expires_in: 100000}), - nock('http://example.com') - .get('/') - .reply(200), + nock('http://example.com').get('/').reply(200), ]; client.credentials = {refresh_token: 'refresh-token-placeholder'}; try { await client.request({url: 'http://example.com'}); + // eslint-disable-next-line no-empty } catch (e) {} await client.request({url: 'http://example.com'}); scopes.forEach(s => s.done()); @@ -1194,9 +1155,7 @@ describe('oauth2', () => { it('should not retry requests with streaming data', done => { const s = fs.createReadStream('./test/fixtures/public.pem'); - const scope = nock('http://example.com') - .post('/') - .reply(401); + const scope = nock('http://example.com').post('/').reply(401); client.credentials = { access_token: 'initial-access-token', refresh_token: 'refresh-token-placeholder', @@ -1381,7 +1340,7 @@ describe('oauth2', () => { access_token: 'initial-access-token', expiry_date: new Date().getTime() - 1000, }); - await assertRejects( + await assert.rejects( client.getRequestHeaders('http://example.com'), /No refresh token is set./ ); diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 59a3c7e3..be1c93ed 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -15,7 +15,6 @@ import * as assert from 'assert'; import {describe, it, afterEach} from 'mocha'; import {GaxiosOptions} from 'gaxios'; -const assertRejects = require('assert-rejects'); import * as nock from 'nock'; import {DefaultTransporter, RequestError} from '../src/transporters'; @@ -111,9 +110,7 @@ describe('transporters', () => { it('should return an error for a 404 response', done => { const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(404, 'Not found'); + const scope = nock(url).get('/').reply(404, 'Not found'); transporter.request({url}, error => { scope.done(); assert.strictEqual(error!.message, 'Not found'); @@ -148,9 +145,7 @@ describe('transporters', () => { it('should support invocation with async/await', async () => { const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(200); + const scope = nock(url).get('/').reply(200); const res = await transporter.request({url}); scope.done(); assert.strictEqual(res.status, 200); @@ -158,18 +153,14 @@ describe('transporters', () => { it('should throw if using async/await', async () => { const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(500, '🦃'); - await assertRejects(transporter.request({url}), /🦃/); + const scope = nock(url).get('/').reply(500, '🦃'); + await assert.rejects(transporter.request({url}), /🦃/); scope.done(); }); it('should work with a callback', done => { const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(200); + const scope = nock(url).get('/').reply(200); transporter.request({url}, (err, res) => { scope.done(); assert.strictEqual(err, null); diff --git a/tsconfig.json b/tsconfig.json index f674b084..e4058efd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { - "lib": ["es2015", "dom"], + "lib": ["es2018", "dom"], "rootDir": ".", "outDir": "build" }, From a79def58faab1b84915952165881027234b89dbc Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 26 Mar 2020 20:06:07 +0100 Subject: [PATCH 116/662] Update dependency gtoken to v5 (#925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [gtoken](https://togithub.com/google/node-gtoken) | dependencies | major | [`^4.1.0` -> `^5.0.0`](https://renovatebot.com/diffs/npm/gtoken/4.1.4/5.0.0) | --- ### Release Notes
google/node-gtoken ### [`v5.0.0`](https://togithub.com/google/node-gtoken/blob/master/CHANGELOG.md#​500httpswwwgithubcomgoogleapisnode-gtokencomparev414v500-2020-03-24) [Compare Source](https://togithub.com/google/node-gtoken/compare/v4.1.4...v5.0.0) ##### ⚠ BREAKING CHANGES - drop Node 8 from engines ([#​284](https://togithub.com/google/node-gtoken/issues/284)) - typescript@3.7.x introduced breaking changes to compiled code ##### Features - drop Node 8 from engines ([#​284](https://www.github.com/googleapis/node-gtoken/issues/284)) ([209e007](https://www.github.com/googleapis/node-gtoken/commit/209e00746116a82a3cf9acc158aff12a4971f3d0)) ##### Build System - update gts and typescript ([#​283](https://www.github.com/googleapis/node-gtoken/issues/283)) ([ff076dc](https://www.github.com/googleapis/node-gtoken/commit/ff076dcb3da229238e7bed28d739c48986652c78)) ##### [4.1.4](https://www.github.com/googleapis/node-gtoken/compare/v4.1.3...v4.1.4) (2020-01-06) ##### Bug Fixes - **deps:** pin TypeScript below 3.7.0 ([f1ae7b6](https://www.github.com/googleapis/node-gtoken/commit/f1ae7b64ead1c918546ae5bbe8546dfb4ecc788a)) - **deps:** update dependency jws to v4 ([#​251](https://www.github.com/googleapis/node-gtoken/issues/251)) ([e13542f](https://www.github.com/googleapis/node-gtoken/commit/e13542f888a81ed3ced0023e9b78ed25264b1d1c)) ##### [4.1.3](https://www.github.com/googleapis/node-gtoken/compare/v4.1.2...v4.1.3) (2019-11-15) ##### Bug Fixes - **deps:** use typescript ~3.6.0 ([#​246](https://www.github.com/googleapis/node-gtoken/issues/246)) ([5f725b7](https://www.github.com/googleapis/node-gtoken/commit/5f725b71f080e83058b1a23340acadc0c8704123)) ##### [4.1.2](https://www.github.com/googleapis/node-gtoken/compare/v4.1.1...v4.1.2) (2019-11-13) ##### Bug Fixes - **docs:** add jsdoc-region-tag plugin ([#​242](https://www.github.com/googleapis/node-gtoken/issues/242)) ([994c5cc](https://www.github.com/googleapis/node-gtoken/commit/994c5ccf92731599aa63b84c29a9d5f6b1431cc5)) ##### [4.1.1](https://www.github.com/googleapis/node-gtoken/compare/v4.1.0...v4.1.1) (2019-10-31) ##### Bug Fixes - **deps:** update gaxios to 2.1.0 ([#​238](https://www.github.com/googleapis/node-gtoken/issues/238)) ([bb12064](https://www.github.com/googleapis/node-gtoken/commit/bb1206420388399ef8992efe54c70bdb3fdcd965))
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cfe20ee5..cba9d13e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "fast-text-encoding": "^1.0.0", "gaxios": "^3.0.0", "gcp-metadata": "^4.0.0", - "gtoken": "^4.1.0", + "gtoken": "^5.0.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" }, From 41b0cb7b3dbae1b264b7de1cae82648c402d5cc3 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2020 19:14:07 +0000 Subject: [PATCH 117/662] chore: release 6.0.0 (#920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release \*beep\* \*boop\* --- ## [6.0.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.10.1...v6.0.0) (2020-03-26) ### ⚠ BREAKING CHANGES * typescript@3.7.x introduced some breaking changes in generated code. * require node 10 in engines field (#926) * remove deprecated methods (#906) ### Features * require node 10 in engines field ([#926](https://www.github.com/googleapis/google-auth-library-nodejs/issues/926)) ([d89c59a](https://www.github.com/googleapis/google-auth-library-nodejs/commit/d89c59a316e9ca5b8c351128ee3e2d91e9729d5c)) ### Bug Fixes * do not warn for SDK creds ([#905](https://www.github.com/googleapis/google-auth-library-nodejs/issues/905)) ([9536840](https://www.github.com/googleapis/google-auth-library-nodejs/commit/9536840f88e77f747bbbc2c1b5b4289018fc23c9)) * use iamcredentials API to sign blobs ([#908](https://www.github.com/googleapis/google-auth-library-nodejs/issues/908)) ([7b8e4c5](https://www.github.com/googleapis/google-auth-library-nodejs/commit/7b8e4c52e31bb3d448c3ff8c05002188900eaa04)) * **deps:** update dependency gaxios to v3 ([#917](https://www.github.com/googleapis/google-auth-library-nodejs/issues/917)) ([1f4bf61](https://www.github.com/googleapis/google-auth-library-nodejs/commit/1f4bf6128a0dcf22cfe1ec492b2192f513836cb2)) * **deps:** update dependency gcp-metadata to v4 ([#918](https://www.github.com/googleapis/google-auth-library-nodejs/issues/918)) ([d337131](https://www.github.com/googleapis/google-auth-library-nodejs/commit/d337131d009cc1f8182f7a1f8a9034433ee3fbf7)) * **types:** add additional fields to TokenInfo ([#907](https://www.github.com/googleapis/google-auth-library-nodejs/issues/907)) ([5b48eb8](https://www.github.com/googleapis/google-auth-library-nodejs/commit/5b48eb86c108c47d317a0eb96b47c0cae86f98cb)) ### Build System * update to latest gts and TypeScript ([#927](https://www.github.com/googleapis/google-auth-library-nodejs/issues/927)) ([e11e18c](https://www.github.com/googleapis/google-auth-library-nodejs/commit/e11e18cb33eb60a666980d061c54bb8891cdd242)) ### Miscellaneous Chores * remove deprecated methods ([#906](https://www.github.com/googleapis/google-auth-library-nodejs/issues/906)) ([f453fb7](https://www.github.com/googleapis/google-auth-library-nodejs/commit/f453fb7d8355e6dc74800b18d6f43c4e91d4acc9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b3f358c..9016673d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [6.0.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.10.1...v6.0.0) (2020-03-26) + + +### ⚠ BREAKING CHANGES + +* typescript@3.7.x introduced some breaking changes in +generated code. +* require node 10 in engines field (#926) +* remove deprecated methods (#906) + +### Features + +* require node 10 in engines field ([#926](https://www.github.com/googleapis/google-auth-library-nodejs/issues/926)) ([d89c59a](https://www.github.com/googleapis/google-auth-library-nodejs/commit/d89c59a316e9ca5b8c351128ee3e2d91e9729d5c)) + + +### Bug Fixes + +* do not warn for SDK creds ([#905](https://www.github.com/googleapis/google-auth-library-nodejs/issues/905)) ([9536840](https://www.github.com/googleapis/google-auth-library-nodejs/commit/9536840f88e77f747bbbc2c1b5b4289018fc23c9)) +* use iamcredentials API to sign blobs ([#908](https://www.github.com/googleapis/google-auth-library-nodejs/issues/908)) ([7b8e4c5](https://www.github.com/googleapis/google-auth-library-nodejs/commit/7b8e4c52e31bb3d448c3ff8c05002188900eaa04)) +* **deps:** update dependency gaxios to v3 ([#917](https://www.github.com/googleapis/google-auth-library-nodejs/issues/917)) ([1f4bf61](https://www.github.com/googleapis/google-auth-library-nodejs/commit/1f4bf6128a0dcf22cfe1ec492b2192f513836cb2)) +* **deps:** update dependency gcp-metadata to v4 ([#918](https://www.github.com/googleapis/google-auth-library-nodejs/issues/918)) ([d337131](https://www.github.com/googleapis/google-auth-library-nodejs/commit/d337131d009cc1f8182f7a1f8a9034433ee3fbf7)) +* **types:** add additional fields to TokenInfo ([#907](https://www.github.com/googleapis/google-auth-library-nodejs/issues/907)) ([5b48eb8](https://www.github.com/googleapis/google-auth-library-nodejs/commit/5b48eb86c108c47d317a0eb96b47c0cae86f98cb)) + + +### Build System + +* update to latest gts and TypeScript ([#927](https://www.github.com/googleapis/google-auth-library-nodejs/issues/927)) ([e11e18c](https://www.github.com/googleapis/google-auth-library-nodejs/commit/e11e18cb33eb60a666980d061c54bb8891cdd242)) + + +### Miscellaneous Chores + +* remove deprecated methods ([#906](https://www.github.com/googleapis/google-auth-library-nodejs/issues/906)) ([f453fb7](https://www.github.com/googleapis/google-auth-library-nodejs/commit/f453fb7d8355e6dc74800b18d6f43c4e91d4acc9)) + ### [5.10.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.10.0...v5.10.1) (2020-02-25) diff --git a/package.json b/package.json index cba9d13e..3127addf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "5.10.1", + "version": "6.0.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index d56e7858..fc62bf43 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.10.1", + "google-auth-library": "^6.0.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 81fdabe6fb7f0b8c5114c0d835680a28def822e2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 27 Mar 2020 20:39:55 +0100 Subject: [PATCH 118/662] fix(deps): update dependency google-auth-library to v6 (#930) --- samples/puppeteer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 7ed09726..fe95f6d0 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -11,7 +11,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^5.0.0", + "google-auth-library": "^6.0.0", "puppeteer": "^2.0.0" } } From 3bc13a75784d9fe6ddbbe6688e5a063427e18df1 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 1 Apr 2020 14:04:42 -0700 Subject: [PATCH 119/662] build: update templates --- .prettierignore | 8 +++----- synth.metadata | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.prettierignore b/.prettierignore index a4ac7b37..f6fac98b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,3 @@ -**/node_modules -**/.coverage -build/ -docs/ -protos/ +node_modules/* +samples/node_modules/* +src/**/doc/* diff --git a/synth.metadata b/synth.metadata index fc4b8607..5eb937cb 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,11 +1,12 @@ { - "updateTime": "2020-03-22T11:10:18.576330Z", + "updateTime": "2020-04-01T11:19:17.927253Z", "sources": [ { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "7e98e1609c91082f4eeb63b530c6468aefd18cfd" + "sha": "99820243d348191bc9c634f2b48ddf65096285ed", + "log": "99820243d348191bc9c634f2b48ddf65096285ed\nfix: update template files for Node.js libraries (#463)\n\n\n3cbe6bcd5623139ac9834c43818424ddca5430cb\nfix(ruby): remove dead troubleshooting link from generated auth guide (#462)\n\n\na003d8655d3ebec2bbbd5fc3898e91e152265c67\ndocs: remove \"install stable\" instructions (#461)\n\nThe package hasn't been released to PyPI in some time\nf5e8c88d9870d8aa4eb43fa0b39f07e02bfbe4df\nchore(python): add license headers to config files; make small tweaks to templates (#458)\n\n\ne36822bfa0acb355502dab391b8ef9c4f30208d8\nchore(java): treat samples shared configuration dependency update as chore (#457)\n\n\n1b4cc80a7aaf164f6241937dd87f3bd1f4149e0c\nfix: do not run node 8 CI (#456)\n\n\nee4330a0e5f4b93978e8683fbda8e6d4148326b7\nchore(java_templates): mark version bumps of current library as a chore (#452)\n\nWith the samples/install-without-bom/pom.xml referencing the latest released library, we want to mark updates of this version as a chore for renovate bot.\na0d3133a5d45544a66345059eebf76933265c099\nfix(java): run mvn install with retry (#453)\n\n* fix(java): run mvn install with retry\n\n* fix invocation of command\n6a17abc7652e2fe563e1288c6e8c23fc260dda97\ndocs: document the release schedule we follow (#454)\n\n\n" } } ] From ec71222343fca7ad12d54cf5391adb9699ca2dc2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 1 Apr 2020 23:34:48 +0200 Subject: [PATCH 120/662] chore(deps): update dependency @types/sinon to v9 (#932) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@types/sinon](https://togithub.com/DefinitelyTyped/DefinitelyTyped) | devDependencies | major | [`^7.0.0` -> `^9.0.0`](https://renovatebot.com/diffs/npm/@types%2fsinon/7.5.2/9.0.0) | --- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3127addf..709f5eef 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", "@types/node": "^10.5.1", - "@types/sinon": "^7.0.0", + "@types/sinon": "^9.0.0", "@types/tmp": "^0.1.0", "assert-rejects": "^1.0.0", "c8": "^7.0.0", From 8a3189a719e134547bc100af5f3878637c297162 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 1 Apr 2020 17:40:22 -0700 Subject: [PATCH 121/662] build: set AUTOSYNTH_MULTIPLE_COMMITS=true for context aware commits (#931) --- synth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/synth.py b/synth.py index 2c5cb78d..b297bf3f 100644 --- a/synth.py +++ b/synth.py @@ -2,6 +2,9 @@ import synthtool.gcp as gcp import logging logging.basicConfig(level=logging.DEBUG) + +AUTOSYNTH_MULTIPLE_COMMITS = True + common_templates = gcp.CommonTemplates() templates = common_templates.node_library() s.copy(templates, excludes=["README.md"]) From da6bdb5c79e4d2ae68482bb73494d627569af944 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Sun, 5 Apr 2020 12:48:40 -0700 Subject: [PATCH 122/662] chore: remove duplicate mocha config (#935) --- .mocharc.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .mocharc.json diff --git a/.mocharc.json b/.mocharc.json deleted file mode 100644 index 670c5e2c..00000000 --- a/.mocharc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "enable-source-maps": true, - "throw-deprecation": true, - "timeout": 10000 -} From 53831cf72f6669d13692c5665fef5062dc8f6c1a Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 7 Apr 2020 09:50:52 -0700 Subject: [PATCH 123/662] fix: apache license URL (#468) (#936) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/9c94202f-63a5-4df0-9d76-871a00f99b85/targets --- .prettierrc.js | 2 +- synth.metadata | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.prettierrc.js b/.prettierrc.js index 08cba377..d1b95106 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -4,7 +4,7 @@ // 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 +// https://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, diff --git a/synth.metadata b/synth.metadata index 5eb937cb..ed08eee8 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,12 +1,12 @@ { - "updateTime": "2020-04-01T11:19:17.927253Z", + "updateTime": "2020-04-07T11:30:49.767117Z", "sources": [ { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "99820243d348191bc9c634f2b48ddf65096285ed", - "log": "99820243d348191bc9c634f2b48ddf65096285ed\nfix: update template files for Node.js libraries (#463)\n\n\n3cbe6bcd5623139ac9834c43818424ddca5430cb\nfix(ruby): remove dead troubleshooting link from generated auth guide (#462)\n\n\na003d8655d3ebec2bbbd5fc3898e91e152265c67\ndocs: remove \"install stable\" instructions (#461)\n\nThe package hasn't been released to PyPI in some time\nf5e8c88d9870d8aa4eb43fa0b39f07e02bfbe4df\nchore(python): add license headers to config files; make small tweaks to templates (#458)\n\n\ne36822bfa0acb355502dab391b8ef9c4f30208d8\nchore(java): treat samples shared configuration dependency update as chore (#457)\n\n\n1b4cc80a7aaf164f6241937dd87f3bd1f4149e0c\nfix: do not run node 8 CI (#456)\n\n\nee4330a0e5f4b93978e8683fbda8e6d4148326b7\nchore(java_templates): mark version bumps of current library as a chore (#452)\n\nWith the samples/install-without-bom/pom.xml referencing the latest released library, we want to mark updates of this version as a chore for renovate bot.\na0d3133a5d45544a66345059eebf76933265c099\nfix(java): run mvn install with retry (#453)\n\n* fix(java): run mvn install with retry\n\n* fix invocation of command\n6a17abc7652e2fe563e1288c6e8c23fc260dda97\ndocs: document the release schedule we follow (#454)\n\n\n" + "sha": "1df68ed6735ddce6797d0f83641a731c3c3f75b4", + "log": "1df68ed6735ddce6797d0f83641a731c3c3f75b4\nfix: apache license URL (#468)\n\n\nf4a59efa54808c4b958263de87bc666ce41e415f\nfeat: Add discogapic support for GAPICBazel generation (#459)\n\n* feat: Add discogapic support for GAPICBazel generation\n\n* reformat with black\n\n* Rename source repository variable\n\nCo-authored-by: Jeffrey Rennie \n" } } ] From 462ca9473f2663f8fe6e111af9c4992f827c54af Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 11 Apr 2020 06:44:43 +0200 Subject: [PATCH 124/662] chore(deps): update dependency gts to v2 (#937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [gts](https://togithub.com/google/gts) | devDependencies | major | [`^1.0.0` -> `^2.0.0`](https://renovatebot.com/diffs/npm/gts/1.1.2/2.0.0) | --- ### Release Notes
google/gts ### [`v2.0.0`](https://togithub.com/google/gts/blob/master/CHANGELOG.md#​200-httpswwwgithubcomgooglegtscomparev112v200-2020-04-02) [Compare Source](https://togithub.com/google/gts/compare/v1.1.2...v2.0.0) ##### ⚠ BREAKING CHANGES ⚠ This is a major rewrite of the tool. Based on community guidance, we've switched from using [tslint](https://palantir.github.io/tslint/) to [eslint](https://eslint.org/). _Please read all of the steps below to upgrade_. ##### Configuring `eslint` With the shift to `eslint`, `gts` now will format and lint JavaScript _as well_ as TypeScript. Upgrading will require a number of manual steps. To format JavaScript and TypeScript, you can run: $ npx gts fix To specify only TypeScript: $ npx gts fix '**/*.ts' ##### Delete `tslint.json` This file is no longer used, and can lead to confusion. ##### Create a `.eslintrc.json` Now that we're using eslint, you need to extend the eslint configuration baked into the module. Create a new file named `.eslintrc.json`, and paste the following: ```js { "extends": "./node_modules/gts" } ``` ##### Create a `.eslintignore` The `.eslintignore` file lets you ignore specific directories. This tool now lints and formats JavaScript, so it's _really_ important to ignore your build directory! Here is an example of a `.eslintignore` file: **/node_modules build/ ##### Rule changes The underlying linter was changed, so naturally there are going to be a variety of rule changes along the way. To see the full list, check out [.eslintrc.json](https://togithub.com/google/gts/blob/master/.eslintrc.json). ##### Require Node.js 10.x and up Node.js 8.x is now end of life - this module now requires Ndoe.js 10.x and up. ##### Features - add the eol-last rule ([#​425](https://www.github.com/google/gts/issues/425)) ([50ebd4d](https://www.github.com/google/gts/commit/50ebd4dbaf063615f4c025f567ca28076a734223)) - allow eslintrc to run over tsx files ([#​469](https://www.github.com/google/gts/issues/469)) ([a21db94](https://www.github.com/google/gts/commit/a21db94601def563952d677cb0980a12b6730f4c)) - disable global rule for checking TODO comments ([#​459](https://www.github.com/google/gts/issues/459)) ([96aa84a](https://www.github.com/google/gts/commit/96aa84a0a42181046daa248750cc8fef0c320619)) - override require-atomic-updates ([#​468](https://www.github.com/google/gts/issues/468)) ([8105c93](https://www.github.com/google/gts/commit/8105c9334ee5104b05f6b1b2f150e51419637262)) - prefer single quotes if possible ([#​475](https://www.github.com/google/gts/issues/475)) ([39a2705](https://www.github.com/google/gts/commit/39a2705e51b4b6329a70f91f8293a2d7a363bf5d)) - use eslint instead of tslint ([#​400](https://www.github.com/google/gts/issues/400)) ([b3096fb](https://www.github.com/google/gts/commit/b3096fbd5076d302d93c2307bf627e12c423e726)) ##### Bug Fixes - use .prettierrc.js ([#​437](https://www.github.com/google/gts/issues/437)) ([06efa84](https://www.github.com/google/gts/commit/06efa8444cdf1064b64f3e8d61ebd04f45d90b4c)) - **deps:** update dependency chalk to v4 ([#​477](https://www.github.com/google/gts/issues/477)) ([061d64e](https://www.github.com/google/gts/commit/061d64e29d37b93ce55228937cc100e05ddef352)) - **deps:** update dependency eslint-plugin-node to v11 ([#​426](https://www.github.com/google/gts/issues/426)) ([a394b7c](https://www.github.com/google/gts/commit/a394b7c1f80437f25017ca5c500b968ebb789ece)) - **deps:** update dependency execa to v4 ([#​427](https://www.github.com/google/gts/issues/427)) ([f42ef36](https://www.github.com/google/gts/commit/f42ef36709251553342e655e287e889df72ee3e3)) - **deps:** update dependency prettier to v2 ([#​464](https://www.github.com/google/gts/issues/464)) ([20ef43d](https://www.github.com/google/gts/commit/20ef43d566df17d3c93949ef7db3b72ee9123ca3)) - disable no-use-before-define ([#​431](https://www.github.com/google/gts/issues/431)) ([dea2c22](https://www.github.com/google/gts/commit/dea2c223d1d3a60a1786aa820eebb93be27016a7)) - **deps:** update dependency update-notifier to v4 ([#​403](https://www.github.com/google/gts/issues/403)) ([57393b7](https://www.github.com/google/gts/commit/57393b74c6cf299e8ae09311f0382226b8baa3e3)) - **deps:** upgrade to meow 6.x ([#​423](https://www.github.com/google/gts/issues/423)) ([8f93d00](https://www.github.com/google/gts/commit/8f93d0049337a832d9a22b6ae4e86fd41140ec56)) - align back to the google style guide ([#​440](https://www.github.com/google/gts/issues/440)) ([8bd78c4](https://www.github.com/google/gts/commit/8bd78c4c78526a72400f618a95a987d2a7c1a8db)) - disable empty-function check ([#​467](https://www.github.com/google/gts/issues/467)) ([6455d7a](https://www.github.com/google/gts/commit/6455d7a9d227320d3ffe1b00c9c739b846f339a8)) - drop support for node 8 ([#​422](https://www.github.com/google/gts/issues/422)) ([888c686](https://www.github.com/google/gts/commit/888c68692079065f38ce66ec84472f1f3311a050)) - emit .prettierrc.js with init ([#​462](https://www.github.com/google/gts/issues/462)) ([b114614](https://www.github.com/google/gts/commit/b114614d22ab5560d2d1dd5cb6695968cc80027b)) - enable trailing comma ([#​470](https://www.github.com/google/gts/issues/470)) ([6518f58](https://www.github.com/google/gts/commit/6518f5843d3093e3beb7d3371b56d9aecedf3924)) - include _.tsx and _.jsx in default fix command ([#​473](https://www.github.com/google/gts/issues/473)) ([0509780](https://www.github.com/google/gts/commit/050978005ad089d9b3b5d8895b25ea1175d75db2)) ##### [1.1.2](https://www.github.com/google/gts/compare/v1.1.1...v1.1.2) (2019-11-20) ##### Bug Fixes - **deps:** update to newest prettier (with support for optional chain) ([#​396](https://www.github.com/google/gts/issues/396)) ([ce8ad06](https://www.github.com/google/gts/commit/ce8ad06c8489c44a9e2ed5292382637b3ebb7601)) ##### [1.1.1](https://www.github.com/google/gts/compare/v1.1.0...v1.1.1) (2019-11-11) ##### Bug Fixes - **deps:** update dependency chalk to v3 ([#​389](https://www.github.com/google/gts/issues/389)) ([1ce0f45](https://www.github.com/google/gts/commit/1ce0f450677e143a27efc39def617d13c66503e8)) - **deps:** update dependency inquirer to v7 ([#​377](https://www.github.com/google/gts/issues/377)) ([bf2c349](https://www.github.com/google/gts/commit/bf2c349b2208ac63e551542599ac9cd27b461338)) - **deps:** update dependency rimraf to v3 ([#​374](https://www.github.com/google/gts/issues/374)) ([2058eaa](https://www.github.com/google/gts/commit/2058eaa682f4baae978b469fd708d1f866e7da74)) - **deps:** update dependency write-file-atomic to v3 ([#​353](https://www.github.com/google/gts/issues/353)) ([59e6aa8](https://www.github.com/google/gts/commit/59e6aa8580a2f8e9457d2d2b6fa9e18e86347592))
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- system-test/fixtures/kitchen/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index 77ebaeb0..efabbaae 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@types/node": "^10.3.0", "typescript": "^3.0.0", - "gts": "^1.0.0", + "gts": "^2.0.0", "null-loader": "^3.0.0", "ts-loader": "^6.0.0", "webpack": "^4.20.2", From e77b1bcb06fb838073b9f34e5e2896b7ffc170eb Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Sat, 11 Apr 2020 19:16:20 -0700 Subject: [PATCH 125/662] build: remove unused codecov config (#939) --- codecov.yaml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 codecov.yaml diff --git a/codecov.yaml b/codecov.yaml deleted file mode 100644 index 5724ea94..00000000 --- a/codecov.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -codecov: - ci: - - source.cloud.google.com From efe4dac92f63596015f7ea41187a5c3f9ab241a0 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 13 Apr 2020 18:35:33 +0200 Subject: [PATCH 126/662] chore(deps): update dependency karma to v5 (#938) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 709f5eef..40598485 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "execa": "^4.0.0", "gts": "^2.0.0-alpha.8", "is-docker": "^2.0.0", - "karma": "^4.0.0", + "karma": "^5.0.0", "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^2.0.0", "karma-firefox-launcher": "^1.1.0", From b7e8ea881ac95a0f31ec71ee1ef59fc98610f743 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 13 Apr 2020 14:53:12 -0700 Subject: [PATCH 127/662] chore: update lint ignore files (#940) --- .eslintignore | 3 ++- .prettierignore | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.eslintignore b/.eslintignore index 09b31fe7..9340ad9b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ **/node_modules -src/**/doc/* +**/coverage +test/fixtures build/ docs/ protos/ diff --git a/.prettierignore b/.prettierignore index f6fac98b..9340ad9b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,6 @@ -node_modules/* -samples/node_modules/* -src/**/doc/* +**/node_modules +**/coverage +test/fixtures +build/ +docs/ +protos/ From 609f32d1d4a09144013fdca9ad5ea85292d66e58 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 15 Apr 2020 17:31:20 +0200 Subject: [PATCH 128/662] chore(deps): update dependency ts-loader to v7 (#942) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ts-loader](https://togithub.com/TypeStrong/ts-loader) | devDependencies | major | [`^6.0.0` -> `^7.0.0`](https://renovatebot.com/diffs/npm/ts-loader/6.2.2/7.0.0) | --- ### Release Notes
TypeStrong/ts-loader ### [`v7.0.0`](https://togithub.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md#v700) [Compare Source](https://togithub.com/TypeStrong/ts-loader/compare/v6.2.2...v7.0.0) - [Project reference support enhancements](https://togithub.com/TypeStrong/ts-loader/pull/1076) - thanks [@​sheetalkamat](https://togithub.com/sheetalkamat)! - Following the end of life of Node 8, `ts-loader` no longer supports Node 8 **BREAKING CHANGE**
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- system-test/fixtures/kitchen/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 40598485..8a84bfaa 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "puppeteer": "^2.0.0", "sinon": "^9.0.0", "tmp": "^0.1.0", - "ts-loader": "^6.0.0", + "ts-loader": "^7.0.0", "typescript": "^3.8.3", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index efabbaae..5a2bf077 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -21,7 +21,7 @@ "typescript": "^3.0.0", "gts": "^2.0.0", "null-loader": "^3.0.0", - "ts-loader": "^6.0.0", + "ts-loader": "^7.0.0", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" } From 1f69a137b95526adf44c1907a52534de34881beb Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 15 Apr 2020 18:30:54 +0200 Subject: [PATCH 129/662] chore(deps): update dependency null-loader to v4 (#943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [null-loader](https://togithub.com/webpack-contrib/null-loader) | devDependencies | major | [`^3.0.0` -> `^4.0.0`](https://renovatebot.com/diffs/npm/null-loader/3.0.0/4.0.0) | --- ### Release Notes
webpack-contrib/null-loader ### [`v4.0.0`](https://togithub.com/webpack-contrib/null-loader/blob/master/CHANGELOG.md#​400-httpsgithubcomwebpack-contribnull-loadercomparev300v400-2020-04-15) [Compare Source](https://togithub.com/webpack-contrib/null-loader/compare/v3.0.0...v4.0.0) ##### Bug Fixes - support `webpack@5` ##### ⚠ BREAKING CHANGES - minimum required Nodejs version is `10.13`
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- system-test/fixtures/kitchen/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8a84bfaa..15c01c73 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "mv": "^2.1.1", "ncp": "^2.0.0", "nock": "^12.0.0", - "null-loader": "^3.0.0", + "null-loader": "^4.0.0", "puppeteer": "^2.0.0", "sinon": "^9.0.0", "tmp": "^0.1.0", diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index 5a2bf077..3fb5fae2 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -20,7 +20,7 @@ "@types/node": "^10.3.0", "typescript": "^3.0.0", "gts": "^2.0.0", - "null-loader": "^3.0.0", + "null-loader": "^4.0.0", "ts-loader": "^7.0.0", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" From 6a40715da1700e92f252759ea3407c03a0ea6bf5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 15 Apr 2020 21:28:10 +0200 Subject: [PATCH 130/662] chore(deps): update dependency karma-mocha to v2 (#941) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [karma-mocha](https://togithub.com/karma-runner/karma-mocha) | devDependencies | major | [`^1.3.0` -> `^2.0.0`](https://renovatebot.com/diffs/npm/karma-mocha/1.3.0/2.0.0) | --- ### Release Notes
karma-runner/karma-mocha ### [`v2.0.0`](https://togithub.com/karma-runner/karma-mocha/blob/master/CHANGELOG.md#​200-httpsgithubcomkarma-runnerkarma-mochacomparev130v200-2020-04-14) [Compare Source](https://togithub.com/karma-runner/karma-mocha/compare/v1.3.0...v2.0.0) ##### Features - **ci:** enable semanitic-release ([5a5b6d5](https://togithub.com/karma-runner/karma-mocha/commit/5a5b6d52399eab9a20592e3536b3e2df1b3ce9ce)) - Expose 'pending' status ([e847121](https://togithub.com/karma-runner/karma-mocha/commit/e847121e35f59a498c3b09f87f138621b550629b)), closes [#​109](https://togithub.com/karma-runner/karma-mocha/issues/109) - Update Node.js versions ([fd64f5b](https://togithub.com/karma-runner/karma-mocha/commit/fd64f5bcacf2e0de6eeb24772384442bd6a37bed)) ##### BREAKING CHANGES - drop support for node 8
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15c01c73..127297e3 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^2.0.0", "karma-firefox-launcher": "^1.1.0", - "karma-mocha": "^1.3.0", + "karma-mocha": "^2.0.0", "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^4.0.0", From 4d6fba034cb0e70092656e9aff1ba419fdfca880 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 20 Apr 2020 23:04:29 +0200 Subject: [PATCH 131/662] fix(deps): update dependency puppeteer to v3 (#944) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 127297e3..e95ff5c2 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ncp": "^2.0.0", "nock": "^12.0.0", "null-loader": "^4.0.0", - "puppeteer": "^2.0.0", + "puppeteer": "^3.0.0", "sinon": "^9.0.0", "tmp": "^0.1.0", "ts-loader": "^7.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index fe95f6d0..d1dafe78 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^6.0.0", - "puppeteer": "^2.0.0" + "puppeteer": "^3.0.0" } } From 2746d0e9ec292f7eb11ed18155d61d6a7be5850e Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 20 Apr 2020 14:42:20 -0700 Subject: [PATCH 132/662] build: use codecov's action, now that it's authless (#945) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/73563d93-aea4-4354-9013-d19800d55cda/targets --- .github/workflows/ci.yaml | 20 ++++++++++---------- synth.metadata | 11 ++++++++--- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7138a79a..9465009b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,6 +18,11 @@ jobs: - run: node --version - run: npm install - run: npm test + - name: coverage + uses: codecov/codecov-action@v1 + with: + name: actions ${{ matrix.node }} + fail_ci_if_error: true windows: runs-on: windows-latest steps: @@ -27,6 +32,11 @@ jobs: node-version: 12 - run: npm install - run: npm test + - name: coverage + uses: codecov/codecov-action@v1 + with: + name: actions windows + fail_ci_if_error: true lint: runs-on: ubuntu-latest steps: @@ -45,13 +55,3 @@ jobs: node-version: 12 - run: npm install - run: npm run docs-test - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 13 - - run: npm install - - run: npm test - - run: ./node_modules/.bin/c8 report --reporter=text-lcov | npx codecovorg -a ${{ secrets.CODECOV_API_KEY }} -r $GITHUB_REPOSITORY --pipe diff --git a/synth.metadata b/synth.metadata index ed08eee8..27fcc9e4 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,12 +1,17 @@ { - "updateTime": "2020-04-07T11:30:49.767117Z", "sources": [ + { + "git": { + "name": ".", + "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", + "sha": "6a40715da1700e92f252759ea3407c03a0ea6bf5" + } + }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "1df68ed6735ddce6797d0f83641a731c3c3f75b4", - "log": "1df68ed6735ddce6797d0f83641a731c3c3f75b4\nfix: apache license URL (#468)\n\n\nf4a59efa54808c4b958263de87bc666ce41e415f\nfeat: Add discogapic support for GAPICBazel generation (#459)\n\n* feat: Add discogapic support for GAPICBazel generation\n\n* reformat with black\n\n* Rename source repository variable\n\nCo-authored-by: Jeffrey Rennie \n" + "sha": "19465d3ec5e5acdb01521d8f3bddd311bcbee28d" } } ] From 0ee02cd7548357ed866552f0bfcda189c042b976 Mon Sep 17 00:00:00 2001 From: Alexander Fenster Date: Thu, 23 Apr 2020 19:28:49 -0700 Subject: [PATCH 133/662] chore: update npm scripts and synth.py (#946) Update npm scripts: add clean, prelint, prefix; make sure that lint and fix are set properly. Use post-process feature of synthtool. --- package.json | 5 +++-- synth.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e95ff5c2..35ab8bf5 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "prepare": "npm run compile", "lint": "gts check", "compile": "tsc -p .", - "fix": "gts fix && eslint --fix '**/*.js'", + "fix": "gts fix", "pretest": "npm run compile", "docs": "compodoc src/", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", @@ -89,7 +89,8 @@ "browser-test": "karma start", "docs-test": "linkinator docs", "predocs-test": "npm run docs", - "prelint": "cd samples; npm link ../; npm i" + "prelint": "cd samples; npm link ../; npm install", + "precompile": "gts clean" }, "license": "Apache-2.0" } diff --git a/synth.py b/synth.py index b297bf3f..84d0f33e 100644 --- a/synth.py +++ b/synth.py @@ -1,5 +1,6 @@ import synthtool as s import synthtool.gcp as gcp +import synthtool.languages.node as node import logging logging.basicConfig(level=logging.DEBUG) @@ -8,3 +9,5 @@ common_templates = gcp.CommonTemplates() templates = common_templates.node_library() s.copy(templates, excludes=["README.md"]) +node.install() +node.fix() From d9be8b90ccee06a291ec2a03b6f1f1740c9700a7 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 27 Apr 2020 10:52:55 -0700 Subject: [PATCH 134/662] chore: linting --- samples/verifyIdToken.js | 2 +- synth.metadata | 2 +- test/test.googleauth.ts | 2 +- test/test.transporters.ts | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/verifyIdToken.js b/samples/verifyIdToken.js index c0554c7e..a722df3b 100644 --- a/samples/verifyIdToken.js +++ b/samples/verifyIdToken.js @@ -36,7 +36,7 @@ async function main() { console.log(ticket); // You can use this info to get user information too. - const url = `https://people.googleapis.com/v1/people/me?personFields=names`; + const url = 'https://people.googleapis.com/v1/people/me?personFields=names'; const res = await oAuth2Client.request({url}); console.log(res.data); } diff --git a/synth.metadata b/synth.metadata index 27fcc9e4..b498d8c5 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "6a40715da1700e92f252759ea3407c03a0ea6bf5" + "sha": "0ee02cd7548357ed866552f0bfcda189c042b976" } }, { diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index a813cff3..c0a745dd 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1376,7 +1376,7 @@ describe('googleauth', () => { const {auth, scopes} = mockGCE(); mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); const email = 'google@auth.library'; - const iamUri = `https://iamcredentials.googleapis.com`; + const iamUri = 'https://iamcredentials.googleapis.com'; const iamPath = `/v1/projects/-/serviceAccounts/${email}:signBlob`; const signedBlob = 'erutangis'; const data = 'abc123'; diff --git a/test/test.transporters.ts b/test/test.transporters.ts index be1c93ed..5861dd3d 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -135,9 +135,9 @@ describe('transporters', () => { it('should return an error if you try to use request config options with a promise', async () => { const expected = new RegExp( - `'uri' is not a valid configuration option. Please use 'url' instead. This ` + - `library is using Axios for requests. Please see https://github.com/axios/axios ` + - `to learn more about the valid request options.` + "'uri' is not a valid configuration option. Please use 'url' instead. This " + + 'library is using Axios for requests. Please see https://github.com/axios/axios ' + + 'to learn more about the valid request options.' ); const uri = 'http://example.com/api'; assert.throws(() => transporter.request({uri} as GaxiosOptions), expected); From 0b235bd967054a8886601a1526a5b8d4ba00ce73 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 27 Apr 2020 20:10:14 +0200 Subject: [PATCH 135/662] chore(deps): update dependency tmp to ^0.2.0 (#950) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [tmp](https://togithub.com/raszi/node-tmp) | devDependencies | minor | [`^0.1.0` -> `^0.2.0`](https://renovatebot.com/diffs/npm/tmp/0.1.0/0.2.0) | --- ### Release Notes
raszi/node-tmp ### [`v0.2.0`](https://togithub.com/raszi/node-tmp/blob/master/CHANGELOG.md#tmp-v020) [Compare Source](https://togithub.com/raszi/node-tmp/compare/v0.1.0...v0.2.0) - drop support for node version < v8.17.0 **_BREAKING CHANGE_** node versions < v8.17.0 are no longer supported. - [#​216](https://togithub.com/raszi/node-tmp/issues/216) **_BREAKING CHANGE_** SIGINT handler has been removed. Users must install their own SIGINT handler and call process.exit() so that tmp's process exit handler can do the cleanup. A simple handler would be process.on('SIGINT', process.exit); - [#​156](https://togithub.com/raszi/node-tmp/issues/156) **_BREAKING CHANGE_** template option no longer accepts arbitrary paths. all paths must be relative to os.tmpdir(). the template option can point to an absolute path that is located under os.tmpdir(). this can now be used in conjunction with the dir option. - [#​207](https://togithub.com/raszi/node-tmp/issues/TBD) **_BREAKING CHANGE_** dir option no longer accepts arbitrary paths. all paths must be relative to os.tmpdir(). the dir option can point to an absolute path that is located under os.tmpdir(). - [#​218](https://togithub.com/raszi/node-tmp/issues/TBD) **_BREAKING CHANGE_** name option no longer accepts arbitrary paths. name must no longer contain a path and will always be made relative to the current os.tmpdir() and the optional dir option. - [#​197](https://togithub.com/raszi/node-tmp/issues/197) **_BUG FIX_** sync cleanup callback must be returned when using the sync API functions. fs.rmdirSync() must not be called with a second parameter that is a function. - [#​176](https://togithub.com/raszi/node-tmp/issues/176) **_BUG FIX_** fail early if no os.tmpdir() was specified. previous versions of Electron did return undefined when calling os.tmpdir(). \_getTmpDir() now tries to resolve the path returned by os.tmpdir(). now using rimraf for removing directory trees. - [#​246](https://togithub.com/raszi/node-tmp/issues/246) **_BUG FIX_** os.tmpdir() might return a value that includes single or double quotes, similarly so the dir option, the template option and the name option - [#​240](https://togithub.com/raszi/node-tmp/issues/240) **_DOCUMENTATION_** better documentation for `tmp.setGracefulCleanup()`. - [#​206](https://togithub.com/raszi/node-tmp/issues/206) **_DOCUMENTATION_** document name option. - [#​236](https://togithub.com/raszi/node-tmp/issues/236) **_DOCUMENTATION_** document discardDescriptor option. - [#​237](https://togithub.com/raszi/node-tmp/issues/237) **_DOCUMENTATION_** document detachDescriptor option. - [#​238](https://togithub.com/raszi/node-tmp/issues/238) **_DOCUMENTATION_** document mode option. - [#​175](https://togithub.com/raszi/node-tmp/issues/175) **_DOCUMENTATION_** document unsafeCleanup option. ##### Miscellaneous - stabilized tests - general clean up - update jsdoc
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 35ab8bf5..a52a3d1d 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "null-loader": "^4.0.0", "puppeteer": "^3.0.0", "sinon": "^9.0.0", - "tmp": "^0.1.0", + "tmp": "^0.2.0", "ts-loader": "^7.0.0", "typescript": "^3.8.3", "webpack": "^4.20.2", From 481f2c0b387bf6a24bfbec98ca4cd38edf347015 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 1 May 2020 20:12:20 +0200 Subject: [PATCH 136/662] chore(deps): update dependency @types/tmp to ^0.2.0 (#953) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@types/tmp](https://togithub.com/DefinitelyTyped/DefinitelyTyped) | devDependencies | minor | [`^0.1.0` -> `^0.2.0`](https://renovatebot.com/diffs/npm/@types%2ftmp/0.1.0/0.2.0) | --- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a52a3d1d..9022577b 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@types/ncp": "^2.0.1", "@types/node": "^10.5.1", "@types/sinon": "^9.0.0", - "@types/tmp": "^0.1.0", + "@types/tmp": "^0.2.0", "assert-rejects": "^1.0.0", "c8": "^7.0.0", "chai": "^4.2.0", From 07955406e85f07fb60821192253205b9f4137ab9 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 4 May 2020 09:22:17 -0700 Subject: [PATCH 137/662] chore: unpin version of gts (#954) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9022577b..ae77496f 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "chai": "^4.2.0", "codecov": "^3.0.2", "execa": "^4.0.0", - "gts": "^2.0.0-alpha.8", + "gts": "^2.0.0", "is-docker": "^2.0.0", "karma": "^5.0.0", "karma-chrome-launcher": "^3.0.0", From 89e16c2401101d086a8f9d05b8f0771b5c74157c Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Wed, 6 May 2020 18:39:14 -0700 Subject: [PATCH 138/662] fix: gcp-metadata now warns rather than throwing (#956) --- package.json | 2 +- test/test.googleauth.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ae77496f..817006e6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^3.0.0", - "gcp-metadata": "^4.0.0", + "gcp-metadata": "^4.1.0", "gtoken": "^5.0.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index c0a745dd..1d894032 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1045,11 +1045,11 @@ describe('googleauth', () => { scopes.forEach(s => s.done()); }); - it('_checkIsGCE should throw on unexpected errors', async () => { + it('_checkIsGCE should return false on unexpected errors', async () => { assert.notStrictEqual(true, auth.isGCE); const scope = nock500GCE(); - await assert.rejects(auth._checkIsGCE()); - assert.strictEqual(undefined, auth.isGCE); + assert.strictEqual(await auth._checkIsGCE(), false); + assert.strictEqual(auth.isGCE, false); scope.done(); }); From 81a9133a10ab16b889b84b25564ca9658d96aa16 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 8 May 2020 11:29:31 -0700 Subject: [PATCH 139/662] build: do not fail builds on codecov errors (#528) (#957) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/13e8b547-7af0-436b-b85e-2c1942f8f36a/targets Source-Link: https://github.com/googleapis/synthtool/commit/be74d3e532faa47eb59f1a0eaebde0860d1d8ab4 --- .github/workflows/ci.yaml | 4 ++-- synth.metadata | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9465009b..5e73bb3d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: uses: codecov/codecov-action@v1 with: name: actions ${{ matrix.node }} - fail_ci_if_error: true + fail_ci_if_error: false windows: runs-on: windows-latest steps: @@ -36,7 +36,7 @@ jobs: uses: codecov/codecov-action@v1 with: name: actions windows - fail_ci_if_error: true + fail_ci_if_error: false lint: runs-on: ubuntu-latest steps: diff --git a/synth.metadata b/synth.metadata index b498d8c5..7497860f 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "0ee02cd7548357ed866552f0bfcda189c042b976" + "sha": "89e16c2401101d086a8f9d05b8f0771b5c74157c" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "19465d3ec5e5acdb01521d8f3bddd311bcbee28d" + "sha": "be74d3e532faa47eb59f1a0eaebde0860d1d8ab4" } } ] From b94edb0716572593e4e9cb8a9b9bbfa567f71625 Mon Sep 17 00:00:00 2001 From: Alexander Fenster Date: Wed, 20 May 2020 19:14:13 -0700 Subject: [PATCH 140/662] fix: fixing tsc error caused by @types/node update (#965) --- test/test.googleauth.ts | 8 ++++---- test/test.jwt.ts | 6 +++--- test/test.oauth2.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 1d894032..7be33b39 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1032,7 +1032,7 @@ describe('googleauth', () => { const scope = nockNotGCE(); assert.notStrictEqual(true, auth.isGCE); await auth._checkIsGCE(); - assert.strictEqual(false, auth.isGCE); + assert.strictEqual(false as boolean, auth.isGCE); scope.done(); }); @@ -1057,7 +1057,7 @@ describe('googleauth', () => { assert.notStrictEqual(true, auth.isGCE); const scope = nockNotGCE(); await auth._checkIsGCE(); - assert.strictEqual(false, auth.isGCE); + assert.strictEqual(false as boolean, auth.isGCE); scope.done(); }); @@ -1076,9 +1076,9 @@ describe('googleauth', () => { assert.notStrictEqual(true, auth.isGCE); const scope = nockNotGCE(); await auth._checkIsGCE(); - assert.strictEqual(false, auth.isGCE); + assert.strictEqual(false as boolean, auth.isGCE); await auth._checkIsGCE(); - assert.strictEqual(false, auth.isGCE); + assert.strictEqual(false as boolean, auth.isGCE); scope.done(); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index ac643dd4..b8165535 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -393,7 +393,7 @@ describe('jwt', () => { jwt.request({url: 'http://bar'}, () => { assert.strictEqual('initial-access-token', jwt.credentials.access_token); - assert.strictEqual(false, scope.isDone()); + assert.strictEqual(false as boolean, scope.isDone()); done(); }); }); @@ -439,7 +439,7 @@ describe('jwt', () => { jwt.request({url: 'http://bar'}, () => { assert.strictEqual('initial-access-token', jwt.credentials.access_token); - assert.strictEqual(false, scope.isDone()); + assert.strictEqual(false as boolean, scope.isDone()); done(); }); }); @@ -460,7 +460,7 @@ describe('jwt', () => { jwt.request({url: 'http://bar'}, () => { assert.strictEqual('initial-access-token', jwt.credentials.access_token); - assert.strictEqual(false, scope.isDone()); + assert.strictEqual(false as boolean, scope.isDone()); done(); }); }); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 718f91c8..7a48ef78 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1067,7 +1067,7 @@ describe('oauth2', () => { 'initial-access-token', client.credentials.access_token ); - assert.strictEqual(false, scopes[0].isDone()); + assert.strictEqual(false as boolean, scopes[0].isDone()); scopes[1].done(); }); @@ -1083,7 +1083,7 @@ describe('oauth2', () => { 'initial-access-token', client.credentials.access_token ); - assert.strictEqual(false, scopes[0].isDone()); + assert.strictEqual(false as boolean, scopes[0].isDone()); scopes[1].done(); done(); }); @@ -1100,7 +1100,7 @@ describe('oauth2', () => { 'initial-access-token', client.credentials.access_token ); - assert.strictEqual(false, scopes[0].isDone()); + assert.strictEqual(false as boolean, scopes[0].isDone()); scopes[1].done(); done(); }); From 9429dfd6636c8b47f4467907f394f6f5497d19cc Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2020 14:50:46 -0700 Subject: [PATCH 141/662] chore: release 6.0.1 (#934) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9016673d..ae354877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.0.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.0...v6.0.1) (2020-05-21) + + +### Bug Fixes + +* **deps:** update dependency google-auth-library to v6 ([#930](https://www.github.com/googleapis/google-auth-library-nodejs/issues/930)) ([81fdabe](https://www.github.com/googleapis/google-auth-library-nodejs/commit/81fdabe6fb7f0b8c5114c0d835680a28def822e2)) +* apache license URL ([#468](https://www.github.com/googleapis/google-auth-library-nodejs/issues/468)) ([#936](https://www.github.com/googleapis/google-auth-library-nodejs/issues/936)) ([53831cf](https://www.github.com/googleapis/google-auth-library-nodejs/commit/53831cf72f6669d13692c5665fef5062dc8f6c1a)) +* **deps:** update dependency puppeteer to v3 ([#944](https://www.github.com/googleapis/google-auth-library-nodejs/issues/944)) ([4d6fba0](https://www.github.com/googleapis/google-auth-library-nodejs/commit/4d6fba034cb0e70092656e9aff1ba419fdfca880)) +* fixing tsc error caused by @types/node update ([#965](https://www.github.com/googleapis/google-auth-library-nodejs/issues/965)) ([b94edb0](https://www.github.com/googleapis/google-auth-library-nodejs/commit/b94edb0716572593e4e9cb8a9b9bbfa567f71625)) +* gcp-metadata now warns rather than throwing ([#956](https://www.github.com/googleapis/google-auth-library-nodejs/issues/956)) ([89e16c2](https://www.github.com/googleapis/google-auth-library-nodejs/commit/89e16c2401101d086a8f9d05b8f0771b5c74157c)) + ## [6.0.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v5.10.1...v6.0.0) (2020-03-26) diff --git a/package.json b/package.json index 817006e6..b3f32932 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.0", + "version": "6.0.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index fc62bf43..6748efee 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.0", + "google-auth-library": "^6.0.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From e0c3810bec78569aa34afa6909b6806fae9fb8bf Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 9 Jun 2020 17:38:47 -0700 Subject: [PATCH 142/662] build: migrate to secret manager (#969) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/9b55eba7-85ee-48d5-a737-8b677439db4d/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/1c92077459db3dc50741e878f98b08c6261181e0 --- .kokoro/populate-secrets.sh | 32 ++++++++++++++++++++++++++++++++ .kokoro/publish.sh | 2 +- .kokoro/release/publish.cfg | 10 +++------- .kokoro/trampoline.sh | 1 + synth.metadata | 4 ++-- 5 files changed, 39 insertions(+), 10 deletions(-) create mode 100755 .kokoro/populate-secrets.sh diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh new file mode 100755 index 00000000..85801f40 --- /dev/null +++ b/.kokoro/populate-secrets.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Copyright 2020 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. + +set -eo pipefail + +# Populates requested secrets set in SECRET_MANAGER_KEYS from service account: +# kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com +SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" +mkdir -p ${SECRET_LOCATION} +for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") +do + docker run --entrypoint=gcloud \ + --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \ + gcr.io/google.com/cloudsdktool/cloud-sdk \ + secrets versions access latest \ + --credential-file-override=${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json \ + --project cloud-devrel-kokoro-resources \ + --secret $key > \ + "$SECRET_LOCATION/$key" +done diff --git a/.kokoro/publish.sh b/.kokoro/publish.sh index 15eeff92..24957d71 100755 --- a/.kokoro/publish.sh +++ b/.kokoro/publish.sh @@ -24,7 +24,7 @@ python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source / cd $(dirname $0)/.. -NPM_TOKEN=$(cat $KOKORO_KEYSTORE_DIR/73713_google-auth-library-npm-token) +NPM_TOKEN=$(cat $KOKORO_GFILE_DIR/secret_manager/npm_publish_token echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_TOKEN}" > ~/.npmrc npm install diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index 96f179ef..3bed518a 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -47,13 +47,9 @@ before_action { } } -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "google-auth-library-npm-token" - } - } +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "npm_publish_token" } # Download trampoline resources. diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh index 9bd4905c..a4241db2 100755 --- a/.kokoro/trampoline.sh +++ b/.kokoro/trampoline.sh @@ -24,4 +24,5 @@ function cleanup() { } trap cleanup EXIT +$(dirname $0)/populate-secrets.sh # Secret Manager secrets. python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" diff --git a/synth.metadata b/synth.metadata index 7497860f..3a18f05a 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "89e16c2401101d086a8f9d05b8f0771b5c74157c" + "sha": "9429dfd6636c8b47f4467907f394f6f5497d19cc" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "be74d3e532faa47eb59f1a0eaebde0860d1d8ab4" + "sha": "1c92077459db3dc50741e878f98b08c6261181e0" } } ] From 60f0151ea72c0039ca66139a521450b7b07e3b74 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 11 Jun 2020 17:45:12 +0200 Subject: [PATCH 143/662] chore(deps): update dependency mocha to v8 (#970) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [mocha](https://mochajs.org/) ([source](https://togithub.com/mochajs/mocha)) | devDependencies | major | [`^7.0.0` -> `^8.0.0`](https://renovatebot.com/diffs/npm/mocha/7.2.0/8.0.1) | --- ### Release Notes
mochajs/mocha ### [`v8.0.1`](https://togithub.com/mochajs/mocha/blob/master/CHANGELOG.md#​801--2020-06-10) [Compare Source](https://togithub.com/mochajs/mocha/compare/v8.0.0...v8.0.1) The obligatory patch after a major. #### :bug: Fixes - [#​4328](https://togithub.com/mochajs/mocha/issues/4328): Fix `--parallel` when combined with `--watch` ([**@​boneskull**](https://togithub.com/boneskull)) ### [`v8.0.0`](https://togithub.com/mochajs/mocha/blob/master/CHANGELOG.md#​800--2020-06-10) [Compare Source](https://togithub.com/mochajs/mocha/compare/v7.2.0...v8.0.0) In this major release, Mocha adds the ability to _run tests in parallel_. Better late than never! Please note the **breaking changes** detailed below. Let's welcome [**@​giltayar**](https://togithub.com/giltayar) and [**@​nicojs**](https://togithub.com/nicojs) to the maintenance team! #### :boom: Breaking Changes - [#​4164](https://togithub.com/mochajs/mocha/issues/4164): **Mocha v8.0.0 now requires Node.js v10.0.0 or newer.** Mocha no longer supports the Node.js v8.x line ("Carbon"), which entered End-of-Life at the end of 2019 ([**@​UlisesGascon**](https://togithub.com/UlisesGascon)) - [#​4175](https://togithub.com/mochajs/mocha/issues/4175): Having been deprecated with a warning since v7.0.0, **`mocha.opts` is no longer supported** ([**@​juergba**](https://togithub.com/juergba)) :sparkles: **WORKAROUND:** Replace `mocha.opts` with a [configuration file](https://mochajs.org/#configuring-mocha-nodejs). - [#​4260](https://togithub.com/mochajs/mocha/issues/4260): Remove `enableTimeout()` (`this.enableTimeout()`) from the context object ([**@​craigtaub**](https://togithub.com/craigtaub)) :sparkles: **WORKAROUND:** Replace usage of `this.enableTimeout(false)` in your tests with `this.timeout(0)`. - [#​4315](https://togithub.com/mochajs/mocha/issues/4315): The `spec` option no longer supports a comma-delimited list of files ([**@​juergba**](https://togithub.com/juergba)) :sparkles: **WORKAROUND**: Use an array instead (e.g., `"spec": "foo.js,bar.js"` becomes `"spec": ["foo.js", "bar.js"]`). - [#​4309](https://togithub.com/mochajs/mocha/issues/4309): Drop support for Node.js v13.x line, which is now End-of-Life ([**@​juergba**](https://togithub.com/juergba)) - [#​4282](https://togithub.com/mochajs/mocha/issues/4282): `--forbid-only` will throw an error even if exclusive tests are avoided via `--grep` or other means ([**@​arvidOtt**](https://togithub.com/arvidOtt)) - [#​4223](https://togithub.com/mochajs/mocha/issues/4223): The context object's `skip()` (`this.skip()`) in a "before all" (`before()`) hook will no longer execute subsequent sibling hooks, in addition to hooks in child suites ([**@​juergba**](https://togithub.com/juergba)) - [#​4178](https://togithub.com/mochajs/mocha/issues/4178): Remove previously soft-deprecated APIs ([**@​wnghdcjfe**](https://togithub.com/wnghdcjfe)): - `Mocha.prototype.ignoreLeaks()` - `Mocha.prototype.useColors()` - `Mocha.prototype.useInlineDiffs()` - `Mocha.prototype.hideDiff()` #### :tada: Enhancements - [#​4245](https://togithub.com/mochajs/mocha/issues/4245): Add ability to run tests in parallel for Node.js (see [docs](https://mochajs.org/#parallel-tests)) ([**@​boneskull**](https://togithub.com/boneskull)) :exclamation: See also [#​4244](https://togithub.com/mochajs/mocha/issues/4244); [Root Hook Plugins (docs)](https://mochajs.org/#root-hook-plugins) -- _root hooks must be defined via Root Hook Plugins to work in parallel mode_ - [#​4304](https://togithub.com/mochajs/mocha/issues/4304): `--require` now works with ES modules ([**@​JacobLey**](https://togithub.com/JacobLey)) - [#​4299](https://togithub.com/mochajs/mocha/issues/4299): In some circumstances, Mocha can run ES modules under Node.js v10 -- _use at your own risk!_ ([**@​giltayar**](https://togithub.com/giltayar)) #### :book: Documentation - [#​4246](https://togithub.com/mochajs/mocha/issues/4246): Add documentation for parallel mode and Root Hook plugins ([**@​boneskull**](https://togithub.com/boneskull)) #### :bug: Fixes (All bug fixes in Mocha v8.0.0 are also breaking changes, and are listed above)
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- samples/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b3f32932..81dc4936 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "karma-webpack": "^4.0.0", "keypair": "^1.0.1", "linkinator": "^2.0.0", - "mocha": "^7.0.0", + "mocha": "^8.0.0", "mv": "^2.1.1", "ncp": "^2.0.0", "nock": "^12.0.0", diff --git a/samples/package.json b/samples/package.json index 6748efee..36f6e26f 100644 --- a/samples/package.json +++ b/samples/package.json @@ -19,6 +19,6 @@ }, "devDependencies": { "chai": "^4.2.0", - "mocha": "^7.0.0" + "mocha": "^8.0.0" } } From 6a39ae8d181d65bec0d7fcf063990d530077165c Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 11 Jun 2020 09:58:22 -0700 Subject: [PATCH 144/662] chore(nodejs_templates): add script logging to node_library populate-secrets.sh (#971) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/e306327b-605f-4c07-9420-c106e40c47d5/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/e7034945fbdc0e79d3c57f6e299e5c90b0f11469 --- .kokoro/populate-secrets.sh | 12 ++++++++++++ synth.metadata | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh index 85801f40..e6ce8200 100755 --- a/.kokoro/populate-secrets.sh +++ b/.kokoro/populate-secrets.sh @@ -15,12 +15,19 @@ set -eo pipefail +function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;} +function msg { println "$*" >&2 ;} +function println { printf '%s\n' "$(now) $*" ;} + + # Populates requested secrets set in SECRET_MANAGER_KEYS from service account: # kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" +msg "Creating folder on disk for secrets: ${SECRET_LOCATION}" mkdir -p ${SECRET_LOCATION} for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") do + msg "Retrieving secret ${key}" docker run --entrypoint=gcloud \ --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \ gcr.io/google.com/cloudsdktool/cloud-sdk \ @@ -29,4 +36,9 @@ do --project cloud-devrel-kokoro-resources \ --secret $key > \ "$SECRET_LOCATION/$key" + if [[ $? == 0 ]]; then + msg "Secret written to ${SECRET_LOCATION}/${key}" + else + msg "Error retrieving secret ${key}" + fi done diff --git a/synth.metadata b/synth.metadata index 3a18f05a..e83900d6 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "9429dfd6636c8b47f4467907f394f6f5497d19cc" + "sha": "e0c3810bec78569aa34afa6909b6806fae9fb8bf" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "1c92077459db3dc50741e878f98b08c6261181e0" + "sha": "e7034945fbdc0e79d3c57f6e299e5c90b0f11469" } } ] From ca9ddd8d5a583bf139af4909f2553ab8421cdf0d Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 15 Jun 2020 11:21:49 -0700 Subject: [PATCH 145/662] docs: update JWT sample (#975) --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 09d8fff1..fdaa7381 100644 --- a/README.md +++ b/README.md @@ -250,12 +250,11 @@ const {JWT} = require('google-auth-library'); const keys = require('./jwt.keys.json'); async function main() { - const client = new JWT( - keys.client_email, - null, - keys.private_key, - ['https://www.googleapis.com/auth/cloud-platform'], - ); + const client = new JWT({ + email: keys.client_email, + key: keys.private_key, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; const res = await client.request({url}); console.log(res.data); From ebf9beda30f251da6adb9ec0bf943019ea0171c5 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 15 Jun 2020 17:10:14 -0700 Subject: [PATCH 146/662] fix(types): add locale property to idtoken (#974) Fixes #973 --- src/auth/loginticket.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/auth/loginticket.ts b/src/auth/loginticket.ts index 1a86d2ca..ab4208bf 100644 --- a/src/auth/loginticket.ts +++ b/src/auth/loginticket.ts @@ -179,4 +179,10 @@ export interface TokenPayload { * a hosted domain. */ hd?: string; + + /** + * The user's locale, represented by a BCP 47 language tag. + * Might be provided when a name claim is present. + */ + locale?: string; } From 9ddfb9befedd10d6c82cbf799c1afea9ba10d444 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Jun 2020 03:16:49 +0200 Subject: [PATCH 147/662] fix(deps): update dependency puppeteer to v4 (#976) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 81dc4936..92ebdee2 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ncp": "^2.0.0", "nock": "^12.0.0", "null-loader": "^4.0.0", - "puppeteer": "^3.0.0", + "puppeteer": "^4.0.0", "sinon": "^9.0.0", "tmp": "^0.2.0", "ts-loader": "^7.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index d1dafe78..3b0c04fa 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^6.0.0", - "puppeteer": "^3.0.0" + "puppeteer": "^4.0.0" } } From 17a7e247cdb798ddf3173cf44ab762665cbce0a1 Mon Sep 17 00:00:00 2001 From: Sergio Regueira Date: Tue, 16 Jun 2020 20:24:13 +0200 Subject: [PATCH 148/662] fix(tsc): audience property is not mandatory on verifyIdToken (#972) Co-authored-by: Benjamin E. Coe Co-authored-by: Justin Beckwith --- src/auth/oauth2client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 588ac804..90940d2b 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -384,7 +384,7 @@ export interface RevokeCredentialsResult { export interface VerifyIdTokenOptions { idToken: string; - audience: string | string[]; + audience?: string | string[]; maxExpiry?: number; } @@ -1174,7 +1174,7 @@ export class OAuth2Client extends AuthClient { async verifySignedJwtWithCertsAsync( jwt: string, certs: Certificates | PublicKeys, - requiredAudience: string | string[], + requiredAudience?: string | string[], issuers?: string[], maxExpiry?: number ) { From 79f1350ab6550cccacccbd3dfa0fae81d7c31fa0 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2020 11:38:42 -0700 Subject: [PATCH 149/662] chore: release 6.0.2 (#977) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae354877..1ac1ac0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.0.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.1...v6.0.2) (2020-06-16) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v4 ([#976](https://www.github.com/googleapis/google-auth-library-nodejs/issues/976)) ([9ddfb9b](https://www.github.com/googleapis/google-auth-library-nodejs/commit/9ddfb9befedd10d6c82cbf799c1afea9ba10d444)) +* **tsc:** audience property is not mandatory on verifyIdToken ([#972](https://www.github.com/googleapis/google-auth-library-nodejs/issues/972)) ([17a7e24](https://www.github.com/googleapis/google-auth-library-nodejs/commit/17a7e247cdb798ddf3173cf44ab762665cbce0a1)) +* **types:** add locale property to idtoken ([#974](https://www.github.com/googleapis/google-auth-library-nodejs/issues/974)) ([ebf9bed](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ebf9beda30f251da6adb9ec0bf943019ea0171c5)), closes [#973](https://www.github.com/googleapis/google-auth-library-nodejs/issues/973) + ### [6.0.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.0...v6.0.1) (2020-05-21) diff --git a/package.json b/package.json index 92ebdee2..7acfc1fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.1", + "version": "6.0.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 36f6e26f..7184fa8e 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.1", + "google-auth-library": "^6.0.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 9f311013b52b2b3dc4f1b78eb0da786c70213e4f Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Tue, 16 Jun 2020 12:00:59 -0700 Subject: [PATCH 150/662] chore: add Blunderbuss config (#952) --- .github/blunderbuss.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/blunderbuss.yml diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml new file mode 100644 index 00000000..613fbf77 --- /dev/null +++ b/.github/blunderbuss.yml @@ -0,0 +1,6 @@ +assign_issues: + - sofisl + - bcoe +assign_prs: + - sofisl + - bcoe From 42e60c46ce4a463f4c50bf4226d945c38146398b Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 18 Jun 2020 07:46:48 -0700 Subject: [PATCH 151/662] chore: update node issue template (#978) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/37f383f8-7560-459e-b66c-def10ff830cb/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/b10590a4a1568548dd13cfcea9aa11d40898144b --- .github/ISSUE_TEMPLATE/bug_report.md | 11 ++++++++--- synth.metadata | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7cd24b6c..60125d9d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,13 +8,18 @@ Thanks for stopping by to let us know something could be better! **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. -Please run down the following list and make sure you've tried the usual "quick fixes": +1) Is this a client library issue or a product issue? +This is the client library for . We will only be able to assist with issues that pertain to the behaviors of this library. If the issue you're experiencing is due to the behavior of the product itself, please visit the [ Support page]() to reach the most relevant engineers. +2) Did someone already solve this? - Search the issues already opened: https://github.com/googleapis/google-auth-library-nodejs/issues - Search the issues on our "catch-all" repository: https://github.com/googleapis/google-cloud-node - - Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js + - Search or ask on StackOverflow (engineers monitor these tags): http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js -If you are still having issues, please be sure to include as much information as possible: +3) Do you have a support contract? +Please create an issue in the [support console](https://cloud.google.com/support/) to ensure a timely response. + +If the support paths suggested above still do not result in a resolution, please provide the following details. #### Environment details diff --git a/synth.metadata b/synth.metadata index e83900d6..881c2c89 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "e0c3810bec78569aa34afa6909b6806fae9fb8bf" + "sha": "9f311013b52b2b3dc4f1b78eb0da786c70213e4f" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "e7034945fbdc0e79d3c57f6e299e5c90b0f11469" + "sha": "b10590a4a1568548dd13cfcea9aa11d40898144b" } } ] From 67f6fa58c2bf5ffa3d73f4c45f62054f245f5376 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Sat, 27 Jun 2020 17:47:06 -0700 Subject: [PATCH 152/662] build: add config .gitattributes (#982) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/2a81bca4-7abd-4108-ac1f-21340f858709/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/dc9caca650c77b7039e2bbc3339ffb34ae78e5b7 --- .gitattributes | 3 +++ synth.metadata | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..2e63216a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.ts text eol=lf +*.js test eol=lf +protos/* linguist-generated diff --git a/synth.metadata b/synth.metadata index 881c2c89..f7f37e7f 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "9f311013b52b2b3dc4f1b78eb0da786c70213e4f" + "sha": "42e60c46ce4a463f4c50bf4226d945c38146398b" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "b10590a4a1568548dd13cfcea9aa11d40898144b" + "sha": "dc9caca650c77b7039e2bbc3339ffb34ae78e5b7" } } ] From 35ef9fef2ad83e08abdcf853b977bf8bb626bd06 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 29 Jun 2020 18:42:56 +0200 Subject: [PATCH 153/662] chore(deps): update dependency nock to v13 (#985) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [nock](https://togithub.com/nock/nock) | devDependencies | major | [`^12.0.0` -> `^13.0.0`](https://renovatebot.com/diffs/npm/nock/12.0.3/13.0.0) | --- ### Release Notes
nock/nock ### [`v13.0.0`](https://togithub.com/nock/nock/releases/v13.0.0) [Compare Source](https://togithub.com/nock/nock/compare/v12.0.3...v13.0.0) See the [Migration Guide](https://togithub.com/nock/nock/blob/75507727cf09a0b7bf0aa7ebdf3621952921b82e/migration_guides/migrating_to_13.md) ##### Breaking changes 1. `Scope.log` has been removed. Use the `debug` library when [debugging](https://togithub.com/nock/nock#debugging) failed matches. 2. `socketDelay` has been removed. Use [`delayConnection`](https://togithub.com/nock/nock#delay-the-connection) instead. 3. `delay`, `delayConnection`, and `delayBody` are now setters instead of additive. 4. [When recording](https://togithub.com/nock/nock#recording), skipping body matching using `*` is no longer supported by `nock.define`. Set the definition body to `undefined` instead. 5. `ClientRequest.abort()` has been updated to align with Node's native behavior. This could be considered a feature, however, it created some subtle differences that are not backwards compatible. Refer to the migration guide for details. 6. Playback of a mocked responses will now never happen until the 'socket' event is emitted.
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7acfc1fe..9473f68d 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "mocha": "^8.0.0", "mv": "^2.1.1", "ncp": "^2.0.0", - "nock": "^12.0.0", + "nock": "^13.0.0", "null-loader": "^4.0.0", "puppeteer": "^4.0.0", "sinon": "^9.0.0", From 53105abe8006532fba94126da5da5619831b7a27 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 6 Jul 2020 11:32:26 -0700 Subject: [PATCH 154/662] chore: update CODEOWNERS (#988) --- .github/CODEOWNERS | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..d904d1e2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. +# +# For syntax help see: +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax + + +# The yoshi-nodejs team is the default owner for nodejs repositories. +* @googleapis/yoshi-nodejs From 7cfe6f200b9c04fe4805d1b1e3a2e03a9668e551 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 7 Jul 2020 00:34:35 +0200 Subject: [PATCH 155/662] fix(deps): update dependency puppeteer to v5 (#986) Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9473f68d..93d99a88 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^4.0.0", + "puppeteer": "^5.0.0", "sinon": "^9.0.0", "tmp": "^0.2.0", "ts-loader": "^7.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 3b0c04fa..d32583e0 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^6.0.0", - "puppeteer": "^4.0.0" + "puppeteer": "^5.0.0" } } From b82db030935204c85ec7acc77542c21152aec074 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 16:15:03 -0700 Subject: [PATCH 156/662] chore: release 6.0.3 (#989) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ac1ac0f..b293dd35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.0.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.2...v6.0.3) (2020-07-06) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v5 ([#986](https://www.github.com/googleapis/google-auth-library-nodejs/issues/986)) ([7cfe6f2](https://www.github.com/googleapis/google-auth-library-nodejs/commit/7cfe6f200b9c04fe4805d1b1e3a2e03a9668e551)) + ### [6.0.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.1...v6.0.2) (2020-06-16) diff --git a/package.json b/package.json index 93d99a88..6f148826 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.2", + "version": "6.0.3", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 7184fa8e..3b47cc06 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.2", + "google-auth-library": "^6.0.3", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 88a46b0959e3abb3b784f1fcb7fb4cff4272a0f6 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Tue, 7 Jul 2020 10:56:53 -0700 Subject: [PATCH 157/662] chore: address linting issues (#990) --- src/auth/computeclient.ts | 4 +- src/auth/googleauth.ts | 30 +++++------- src/auth/iam.ts | 2 - src/auth/idtokenclient.ts | 3 +- src/auth/jwtaccess.ts | 5 +- src/auth/jwtclient.ts | 4 +- src/auth/oauth2client.ts | 6 +-- src/auth/refreshclient.ts | 10 ++-- src/messages.ts | 2 +- src/options.ts | 2 +- system-test/fixtures/kitchen/src/index.ts | 18 ++++++- test/test.googleauth.ts | 59 ++++++++--------------- test/test.jwt.ts | 10 ++-- test/test.jwtaccess.ts | 5 +- test/test.oauth2.ts | 12 ++--- test/test.refresh.ts | 12 ++--- 16 files changed, 81 insertions(+), 103 deletions(-) diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 80a27e6d..1bfb4d51 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -16,10 +16,7 @@ import arrify = require('arrify'); import {GaxiosError} from 'gaxios'; import * as gcpMetadata from 'gcp-metadata'; -import * as messages from '../messages'; - import {CredentialRequest, Credentials} from './credentials'; -import {IdTokenProvider} from './idtokenclient'; import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; export interface ComputeOptions extends RefreshOptions { @@ -60,6 +57,7 @@ export class Compute extends OAuth2Client { * @param refreshToken Unused parameter */ protected async refreshTokenNoCache( + // eslint-disable-next-line @typescript-eslint/no-unused-vars refreshToken?: string | null ): Promise { const tokenPath = `service-accounts/${this.serviceAccountEmail}/token`; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 62d7b8e6..fb5065e0 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -21,12 +21,11 @@ import * as path from 'path'; import * as stream from 'stream'; import {createCrypto} from '../crypto/crypto'; -import * as messages from '../messages'; import {DefaultTransporter, Transporter} from '../transporters'; import {Compute, ComputeOptions} from './computeclient'; import {CredentialBody, JWTInput} from './credentials'; -import {IdTokenClient, IdTokenProvider} from './idtokenclient'; +import {IdTokenClient} from './idtokenclient'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; import { @@ -551,22 +550,19 @@ export class GoogleAuth { */ private async getDefaultServiceProjectId(): Promise { return new Promise(resolve => { - exec( - 'gcloud config config-helper --format json', - (err, stdout, stderr) => { - if (!err && stdout) { - try { - const projectId = JSON.parse(stdout).configuration.properties.core - .project; - resolve(projectId); - return; - } catch (e) { - // ignore errors - } + 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); } - ); + resolve(null); + }); }); } @@ -760,7 +756,7 @@ export class GoogleAuth { * HTTP request using the given options. * @param opts Axios request options for the HTTP request. */ - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any async request(opts: GaxiosOptions): Promise> { const client = await this.getClient(); return client.request(opts); diff --git a/src/auth/iam.ts b/src/auth/iam.ts index ede366fd..4c38027f 100644 --- a/src/auth/iam.ts +++ b/src/auth/iam.ts @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as messages from '../messages'; - export interface RequestMetadata { 'x-goog-iam-authority-selector': string; 'x-goog-iam-authorization-token': string; diff --git a/src/auth/idtokenclient.ts b/src/auth/idtokenclient.ts index 9d0d7af5..e123be29 100644 --- a/src/auth/idtokenclient.ts +++ b/src/auth/idtokenclient.ts @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {BodyResponseCallback} from '../transporters'; - import {Credentials} from './credentials'; import {Headers, OAuth2Client, RequestMetadataResponse} from './oauth2client'; @@ -49,6 +47,7 @@ export class IdTokenClient extends OAuth2Client { } protected async getRequestMetadataAsync( + // eslint-disable-next-line @typescript-eslint/no-unused-vars url?: string | null ): Promise { if ( diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index 78f70a43..0139683e 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -16,9 +16,8 @@ import * as jws from 'jws'; import * as LRU from 'lru-cache'; import * as stream from 'stream'; -import * as messages from '../messages'; import {JWTInput} from './credentials'; -import {Headers, RequestMetadataResponse} from './oauth2client'; +import {Headers} from './oauth2client'; const DEFAULT_HEADER: jws.Header = { alg: 'RS256', @@ -150,7 +149,7 @@ export class JWTAccess { callback?: (err?: Error) => void ): void | Promise { if (callback) { - this.fromStreamAsync(inputStream).then(r => callback(), callback); + this.fromStreamAsync(inputStream).then(() => callback(), callback); } else { return this.fromStreamAsync(inputStream); } diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index f6a1ace9..c203cbb5 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -15,7 +15,6 @@ import {GoogleToken} from 'gtoken'; import * as stream from 'stream'; -import * as messages from '../messages'; import {CredentialBody, Credentials, JWTInput} from './credentials'; import {IdTokenProvider} from './idtokenclient'; import {JWTAccess} from './jwtaccess'; @@ -224,6 +223,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { * @private */ protected async refreshTokenNoCache( + // eslint-disable-next-line @typescript-eslint/no-unused-vars refreshToken?: string | null ): Promise { const gtoken = this.createGToken(); @@ -300,7 +300,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { callback?: (err?: Error | null) => void ): void | Promise { if (callback) { - this.fromStreamAsync(inputStream).then(r => callback(), callback); + this.fromStreamAsync(inputStream).then(() => callback(), callback); } else { return this.fromStreamAsync(inputStream); } diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 90940d2b..01edf9fc 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -23,11 +23,10 @@ import * as stream from 'stream'; import * as formatEcdsa from 'ecdsa-sig-formatter'; import {createCrypto, JwkCertificate, hasBrowserCrypto} from '../crypto/crypto'; -import * as messages from '../messages'; import {BodyResponseCallback} from '../transporters'; import {AuthClient} from './authclient'; -import {CredentialRequest, Credentials, JWTInput} from './credentials'; +import {CredentialRequest, Credentials} from './credentials'; import {LoginTicket, TokenPayload} from './loginticket'; /** * The results from the `generateCodeVerifierAsync` method. To learn more, @@ -774,6 +773,7 @@ export class OAuth2Client extends AuthClient { } protected async getRequestMetadataAsync( + // eslint-disable-next-line @typescript-eslint/no-unused-vars url?: string | null ): Promise { const thisCreds = this.credentials; @@ -1138,8 +1138,6 @@ export class OAuth2Client extends AuthClient { } async getIapPublicKeysAsync(): Promise { - const nowTime = new Date().getTime(); - let res: GaxiosResponse; const url: string = OAuth2Client.GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_; diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index b57fbbd9..91d831bd 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -14,12 +14,7 @@ import * as stream from 'stream'; import {JWTInput} from './credentials'; -import { - Headers, - GetTokenResponse, - OAuth2Client, - RefreshOptions, -} from './oauth2client'; +import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; export interface UserRefreshClientOptions extends RefreshOptions { clientId?: string; @@ -76,6 +71,7 @@ export class UserRefreshClient extends OAuth2Client { * @param callback Optional callback. */ protected async refreshTokenNoCache( + // eslint-disable-next-line @typescript-eslint/no-unused-vars refreshToken?: string | null ): Promise { return super.refreshTokenNoCache(this._refreshToken); @@ -135,7 +131,7 @@ export class UserRefreshClient extends OAuth2Client { callback?: (err?: Error) => void ): void | Promise { if (callback) { - this.fromStreamAsync(inputStream).then(r => callback(), callback); + this.fromStreamAsync(inputStream).then(() => callback(), callback); } else { return this.fromStreamAsync(inputStream); } diff --git a/src/messages.ts b/src/messages.ts index 57598e4e..ccfdf083 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -27,7 +27,7 @@ export function warn(warning: Warning) { // @types/node doesn't recognize the emitWarning syntax which // accepts a config object, so `as any` it is // https://nodejs.org/docs/latest-v8.x/api/process.html#process_process_emitwarning_warning_options - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any process.emitWarning(warning.message, warning as any); } else { console.warn(warning.message); diff --git a/src/options.ts b/src/options.ts index eab5d4ba..bda11d9f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -16,7 +16,7 @@ // previous version of the API, it referred to a `Request` options object. // Now it refers to an Axiox Request Config object. This is here to help // ensure users don't pass invalid options when they upgrade from 0.x to 1.x. -// tslint:disable-next-line no-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function validate(options: any) { const vpairs = [ {invalid: 'uri', expected: 'url'}, diff --git a/system-test/fixtures/kitchen/src/index.ts b/system-test/fixtures/kitchen/src/index.ts index 772dd25c..f6b9c9b1 100644 --- a/system-test/fixtures/kitchen/src/index.ts +++ b/system-test/fixtures/kitchen/src/index.ts @@ -1,3 +1,17 @@ +// Copyright 2020 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. + import {GoogleAuth, JWT} from 'google-auth-library'; // uncomment the line below during development // import {GoogleAuth} from '../../../../build/src/index'; @@ -5,8 +19,8 @@ const jwt = new JWT(); const auth = new GoogleAuth(); async function getToken() { const token = await jwt.getToken('token'); - const projectId = await auth.getProjectId(); - const creds = await auth.getApplicationDefault(); + await auth.getProjectId(); + await auth.getApplicationDefault(); return token; } getToken(); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 7be33b39..109998de 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -33,7 +33,6 @@ import {GoogleAuth, JWT, UserRefreshClient, IdTokenClient} from '../src'; import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; import {Compute} from '../src/auth/computeclient'; -import * as messages from '../src/messages'; nock.disableNetConnect(); @@ -221,23 +220,6 @@ describe('googleauth', () => { }; } - function nock404GCE() { - const primary = nock(host).get(instancePath).reply(404); - const secondary = nock(SECONDARY_HOST_ADDRESS) - .get(instancePath) - .reply(404); - return { - done: () => { - try { - primary.done(); - secondary.done(); - } catch (err) { - // secondary can sometimes complete prior to primary. - } - }, - }; - } - function createGetProjectIdNock(projectId = 'not-real') { return nock(host) .get(`${BASE_PATH}/project/project-id`) @@ -259,7 +241,7 @@ describe('googleauth', () => { function mockGCE() { const scope1 = nockIsGCE(); const auth = new GoogleAuth(); - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); const scope2 = nock(HOST_ADDRESS) .get(tokenPath) @@ -283,7 +265,7 @@ describe('googleauth', () => { const auth = new GoogleAuth(); assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (auth as any).fromJSON(null); }); }); @@ -303,7 +285,7 @@ describe('googleauth', () => { it('fromAPIKey should error given an invalid api key', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (auth as any).fromAPIKey(null); }); }); @@ -311,7 +293,7 @@ describe('googleauth', () => { it('should make a request with the api key', async () => { const scope = nock(BASE_URL) .post(ENDPOINT) - .reply(function (uri) { + .reply(function () { assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); return [200, RESPONSE_BODY]; }); @@ -426,7 +408,7 @@ describe('googleauth', () => { it('fromStream should error on null stream', done => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (auth as any).fromStream(null, (err: Error) => { assert.strictEqual(true, err instanceof Error); done(); @@ -515,7 +497,7 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should error on null file path', async () => { try { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any await (auth as any)._getApplicationCredentialsFromFilePath(null); } catch (e) { return; @@ -535,7 +517,7 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should error on non-string file path', async () => { try { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any await auth._getApplicationCredentialsFromFilePath(2 as any); } catch (e) { return; @@ -627,7 +609,7 @@ describe('googleauth', () => { it('tryGetApplicationCredentialsFromEnvironmentVariable should return null when env const is empty string', async () => { // Set up a mock to return an empty path string. - const stub = mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS'); + mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS'); const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); assert.strictEqual(client, null); }); @@ -748,7 +730,7 @@ describe('googleauth', () => { assert.strictEqual(projectId, STUB_PROJECT); // Null out all the private functions that make this method work - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const anyd = auth as any; anyd.getProductionProjectId = null; anyd.getFileProjectId = null; @@ -875,12 +857,12 @@ describe('googleauth', () => { // Make sure our special test bit is not set yet, indicating that // this is a new credentials instance. // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any assert.strictEqual(undefined, (cachedCredential as any).specialTestBit); // Now set the special test bit. // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (cachedCredential as any).specialTestBit = 'monkey'; // Ask for credentials again, from the same auth instance. We expect @@ -893,7 +875,7 @@ describe('googleauth', () => { // the object instance is the same. // Test verifies invalid parameter tests, which requires cast to // any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any assert.strictEqual('monkey', (result2 as any).specialTestBit); assert.strictEqual(cachedCredential, result2); @@ -906,7 +888,7 @@ describe('googleauth', () => { // Make sure we get a new (non-cached) credential instance back. // Test verifies invalid parameter tests, which requires cast to // any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any assert.strictEqual(undefined, (result3 as any).specialTestBit); assert.notStrictEqual(cachedCredential, result3); }); @@ -1289,14 +1271,14 @@ describe('googleauth', () => { it('should get the current environment if GCE', async () => { envDetect.clear(); - const {auth, scopes} = mockGCE(); + const {auth} = mockGCE(); const env = await auth.getEnv(); assert.strictEqual(env, envDetect.GCPEnv.COMPUTE_ENGINE); }); it('should get the current environment if GKE', async () => { envDetect.clear(); - const {auth, scopes} = mockGCE(); + const {auth} = mockGCE(); const scope = nock(host) .get(`${instancePath}/attributes/cluster-name`) .reply(200, {}, HEADERS); @@ -1307,7 +1289,7 @@ describe('googleauth', () => { it('should cache prior call to getEnv(), when GCE', async () => { envDetect.clear(); - const {auth, scopes} = mockGCE(); + const {auth} = mockGCE(); auth.getEnv(); const env = await auth.getEnv(); assert.strictEqual(env, envDetect.GCPEnv.COMPUTE_ENGINE); @@ -1315,7 +1297,7 @@ describe('googleauth', () => { it('should cache prior call to getEnv(), when GKE', async () => { envDetect.clear(); - const {auth, scopes} = mockGCE(); + const {auth} = mockGCE(); const scope = nock(host) .get(`${instancePath}/attributes/cluster-name`) .reply(200, {}, HEADERS); @@ -1402,7 +1384,7 @@ describe('googleauth', () => { }); it('should throw if getProjectId cannot find a projectId', async () => { - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); await assert.rejects( auth.getProjectId(), @@ -1459,7 +1441,7 @@ describe('googleauth', () => { assert(client instanceof UserRefreshClient); const apiReq = nock(BASE_URL) .post(ENDPOINT) - .reply(function (uri) { + .reply(function () { assert.strictEqual( this.req.headers['x-goog-user-project'][0], 'my-quota-project' @@ -1482,6 +1464,7 @@ describe('googleauth', () => { const client = await auth.getIdTokenClient('a-target-audience'); assert(client instanceof IdTokenClient); assert(client.idTokenProvider instanceof Compute); + nockScopes.forEach(s => s.done()); }); it('should return a JWT client for getIdTokenClient', async () => { @@ -1519,7 +1502,7 @@ describe('googleauth', () => { mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id'); try { - const client = await auth.getIdTokenClient('a-target-audience'); + await auth.getIdTokenClient('a-target-audience'); } catch (e) { assert( e.message.startsWith('Cannot fetch ID token in this environment') diff --git a/test/test.jwt.ts b/test/test.jwt.ts index b8165535..14b40477 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -119,7 +119,7 @@ describe('jwt', () => { }); const scope = createGTokenMock({access_token: 'initial-access-token'}); - jwt.authorize((err, creds) => { + jwt.authorize(() => { scope.done(); assert.strictEqual('http://foo', jwt.gtoken!.scope); done(); @@ -574,7 +574,7 @@ describe('jwt', () => { it('fromJson should error on null json', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (jwt as any).fromJSON(null); }); }); @@ -658,7 +658,7 @@ describe('jwt', () => { it('fromStream should error on null stream', done => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (jwt as any).fromStream(null, (err: Error) => { assert.strictEqual(true, err instanceof Error); done(); @@ -693,7 +693,7 @@ describe('jwt', () => { it('fromAPIKey should error without api key', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (jwt as any).fromAPIKey(undefined); }); }); @@ -702,7 +702,7 @@ describe('jwt', () => { const KEY = 'test'; assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any jwt.fromAPIKey({key: KEY} as any); }); }); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index afe57b2a..8618faa1 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -19,7 +19,6 @@ import * as jws from 'jws'; import * as sinon from 'sinon'; import {JWTAccess} from '../src'; -import * as messages from '../src/messages'; // eslint-disable-next-line @typescript-eslint/no-var-requires const keypair = require('keypair'); @@ -100,7 +99,7 @@ describe('jwtaccess', () => { it('fromJson should error on null json', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (client as any).fromJSON(null); }); }); @@ -144,7 +143,7 @@ describe('jwtaccess', () => { it('fromStream should error on null stream', done => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (client as any).fromStream(null, (err: Error) => { assert.strictEqual(true, err instanceof Error); done(); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 7a48ef78..dffa899e 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -22,11 +22,9 @@ import * as nock from 'nock'; import * as path from 'path'; import * as qs from 'querystring'; import * as sinon from 'sinon'; -import * as url from 'url'; import {CodeChallengeMethod, OAuth2Client} from '../src'; import {LoginTicket} from '../src/auth/loginticket'; -import * as messages from '../src/messages'; nock.disableNetConnect(); @@ -189,7 +187,7 @@ describe('oauth2', () => { return new LoginTicket('c', payload); }; assert.throws( - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any () => (client as any).verifyIdToken(idToken, audience), /This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry./ ); @@ -905,7 +903,7 @@ describe('oauth2', () => { it('should not emit warning on refreshAccessToken', async () => { let warned = false; sandbox.stub(process, 'emitWarning').callsFake(() => (warned = true)); - client.refreshAccessToken((err, result) => { + client.refreshAccessToken(() => { assert.strictEqual(warned, false); }); }); @@ -942,7 +940,7 @@ describe('oauth2', () => { raisedEvent = true; }); - client.request({url: 'http://example.com'}, err => { + client.request({url: 'http://example.com'}, () => { scopes.forEach(s => s.done()); assert(raisedEvent); assert.strictEqual(accessToken, client.credentials.access_token); @@ -1118,7 +1116,7 @@ describe('oauth2', () => { access_token: 'initial-access-token', refresh_token: 'refresh-token-placeholder', }; - client.request({url: 'http://example.com/access'}, err => { + client.request({url: 'http://example.com/access'}, () => { scope.done(); scopes[0].done(); assert.strictEqual('abc123', client.credentials.access_token); @@ -1144,7 +1142,7 @@ describe('oauth2', () => { refresh_token: 'refresh-token-placeholder', expiry_date: new Date().getTime() + 500000, }; - client.request({url: 'http://example.com/access'}, err => { + client.request({url: 'http://example.com/access'}, () => { scope.done(); scopes[0].done(); assert.strictEqual('abc123', client.credentials.access_token); diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 847f59b7..8fd59cc5 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -40,7 +40,7 @@ describe('refresh', () => { const refresh = new UserRefreshClient(); assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (refresh as any).fromJSON(null); }); }); @@ -49,7 +49,6 @@ describe('refresh', () => { const refresh = new UserRefreshClient(); assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any refresh.fromJSON({}); }); }); @@ -84,28 +83,28 @@ describe('refresh', () => { it('fromJSON should create UserRefreshClient with clientId_', () => { const json = createJSON(); const refresh = new UserRefreshClient(); - const result = refresh.fromJSON(json); + refresh.fromJSON(json); assert.strictEqual(json.client_id, refresh._clientId); }); it('fromJSON should create UserRefreshClient with clientSecret_', () => { const json = createJSON(); const refresh = new UserRefreshClient(); - const result = refresh.fromJSON(json); + refresh.fromJSON(json); assert.strictEqual(json.client_secret, refresh._clientSecret); }); it('fromJSON should create UserRefreshClient with _refreshToken', () => { const json = createJSON(); const refresh = new UserRefreshClient(); - const result = refresh.fromJSON(json); + refresh.fromJSON(json); assert.strictEqual(json.refresh_token, refresh._refreshToken); }); it('fromStream should error on null stream', done => { const refresh = new UserRefreshClient(); // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any (refresh as any).fromStream(null, (err: Error) => { assert.strictEqual(true, err instanceof Error); done(); @@ -151,5 +150,6 @@ describe('refresh', () => { const headers = await refresh.getRequestHeaders(); assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + req.done(); }); }); From ad12ceb3309b7db7394fe1fe1d5e7b2e4901141d Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 9 Jul 2020 04:58:07 -0700 Subject: [PATCH 158/662] fix: typeo in nodejs .gitattribute (#993) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/cc99acfa-05b8-434b-9500-2f6faf2eaa02/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/799d8e6522c1ef7cb55a70d9ea0b15e045c3d00b --- .gitattributes | 2 +- synth.metadata | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index 2e63216a..d4f4169b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ *.ts text eol=lf -*.js test eol=lf +*.js text eol=lf protos/* linguist-generated diff --git a/synth.metadata b/synth.metadata index f7f37e7f..ddbda19c 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "42e60c46ce4a463f4c50bf4226d945c38146398b" + "sha": "88a46b0959e3abb3b784f1fcb7fb4cff4272a0f6" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "dc9caca650c77b7039e2bbc3339ffb34ae78e5b7" + "sha": "799d8e6522c1ef7cb55a70d9ea0b15e045c3d00b" } } ] From b186d03a71a43561bcc7931b551a39d4f0391813 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2020 01:13:13 +0000 Subject: [PATCH 159/662] chore: release 6.0.4 (#994) :robot: I have created a release \*beep\* \*boop\* --- ### [6.0.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.3...v6.0.4) (2020-07-09) ### Bug Fixes * typeo in nodejs .gitattribute ([#993](https://www.github.com/googleapis/google-auth-library-nodejs/issues/993)) ([ad12ceb](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ad12ceb3309b7db7394fe1fe1d5e7b2e4901141d)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b293dd35..224f99c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.0.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.3...v6.0.4) (2020-07-09) + + +### Bug Fixes + +* typeo in nodejs .gitattribute ([#993](https://www.github.com/googleapis/google-auth-library-nodejs/issues/993)) ([ad12ceb](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ad12ceb3309b7db7394fe1fe1d5e7b2e4901141d)) + ### [6.0.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.2...v6.0.3) (2020-07-06) diff --git a/package.json b/package.json index 6f148826..c9f4ee61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.3", + "version": "6.0.4", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 3b47cc06..69d6ad4f 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.3", + "google-auth-library": "^6.0.4", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 968c172bf1149a61fd100b42c54c9935ebdbd7f0 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 10 Jul 2020 18:46:40 +0200 Subject: [PATCH 160/662] chore(deps): update dependency ts-loader to v8 (#992) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ts-loader](https://togithub.com/TypeStrong/ts-loader) | devDependencies | major | [`^7.0.0` -> `^8.0.0`](https://renovatebot.com/diffs/npm/ts-loader/7.0.5/8.0.0) | --- ### Release Notes
TypeStrong/ts-loader ### [`v8.0.0`](https://togithub.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md#v800) [Compare Source](https://togithub.com/TypeStrong/ts-loader/compare/v7.0.5...v8.0.0) - [Support for symlinks in project references](https://togithub.com/TypeStrong/ts-loader/pull/1136) - thanks [@​sheetalkamat](https://togithub.com/sheetalkamat)! - `ts-loader` now supports TypeScript 3.6 and greater **BREAKING CHANGE**
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- system-test/fixtures/kitchen/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c9f4ee61..4e4ed5f3 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "puppeteer": "^5.0.0", "sinon": "^9.0.0", "tmp": "^0.2.0", - "ts-loader": "^7.0.0", + "ts-loader": "^8.0.0", "typescript": "^3.8.3", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index 3fb5fae2..076408f4 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -21,7 +21,7 @@ "typescript": "^3.0.0", "gts": "^2.0.0", "null-loader": "^4.0.0", - "ts-loader": "^7.0.0", + "ts-loader": "^8.0.0", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" } From 079860f8469ce8462ca6d8793c9f1ac81fc59616 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 12 Jul 2020 18:48:40 +0200 Subject: [PATCH 161/662] chore(deps): update dependency @types/mocha to v8 (#1000) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@types/mocha](https://togithub.com/DefinitelyTyped/DefinitelyTyped) | devDependencies | major | [`^7.0.0` -> `^8.0.0`](https://renovatebot.com/diffs/npm/@types%2fmocha/7.0.2/8.0.0) | --- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e4ed5f3..f26eb6e8 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", "@types/lru-cache": "^5.0.0", - "@types/mocha": "^7.0.0", + "@types/mocha": "^8.0.0", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", "@types/node": "^10.5.1", From 3c07566f0384611227030e9b381fc6e6707e526b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 13 Jul 2020 19:23:16 +0200 Subject: [PATCH 162/662] fix(deps): update dependency lru-cache to v6 (#995) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f26eb6e8..32c8678d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "gcp-metadata": "^4.1.0", "gtoken": "^5.0.0", "jws": "^4.0.0", - "lru-cache": "^5.0.0" + "lru-cache": "^6.0.0" }, "devDependencies": { "@compodoc/compodoc": "^1.1.7", From 3ce427c75c7c92bd4f11954245d873ae98da75ab Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 11:13:40 -0700 Subject: [PATCH 163/662] chore: release 6.0.5 (#1002) * updated CHANGELOG.md [ci skip] * updated package.json [ci skip] * updated samples/package.json Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 224f99c7..52d27d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.0.5](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.4...v6.0.5) (2020-07-13) + + +### Bug Fixes + +* **deps:** update dependency lru-cache to v6 ([#995](https://www.github.com/googleapis/google-auth-library-nodejs/issues/995)) ([3c07566](https://www.github.com/googleapis/google-auth-library-nodejs/commit/3c07566f0384611227030e9b381fc6e6707e526b)) + ### [6.0.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.3...v6.0.4) (2020-07-09) diff --git a/package.json b/package.json index 32c8678d..21e7c7fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.4", + "version": "6.0.5", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 69d6ad4f..970b4075 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.4", + "google-auth-library": "^6.0.5", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 17e96b456362ae1a17eadeee1aa7c675db761941 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 14 Jul 2020 04:59:18 -0700 Subject: [PATCH 164/662] build: missing closing paren in publish script (#1005) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/9c6207e5-a7a6-4e44-ab6b-91751e0230b1/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/d82deccf657a66e31bd5da9efdb96c6fa322fc7e --- .kokoro/publish.sh | 2 +- synth.metadata | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.kokoro/publish.sh b/.kokoro/publish.sh index 24957d71..f056d861 100755 --- a/.kokoro/publish.sh +++ b/.kokoro/publish.sh @@ -24,7 +24,7 @@ python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source / cd $(dirname $0)/.. -NPM_TOKEN=$(cat $KOKORO_GFILE_DIR/secret_manager/npm_publish_token +NPM_TOKEN=$(cat $KOKORO_GFILE_DIR/secret_manager/npm_publish_token) echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_TOKEN}" > ~/.npmrc npm install diff --git a/synth.metadata b/synth.metadata index ddbda19c..5bd58179 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "88a46b0959e3abb3b784f1fcb7fb4cff4272a0f6" + "sha": "3ce427c75c7c92bd4f11954245d873ae98da75ab" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "799d8e6522c1ef7cb55a70d9ea0b15e045c3d00b" + "sha": "d82deccf657a66e31bd5da9efdb96c6fa322fc7e" } } ] From 4ad94be606b332149a10d387a07754232371766a Mon Sep 17 00:00:00 2001 From: "F. Hinkelmann" Date: Tue, 14 Jul 2020 14:44:09 -0400 Subject: [PATCH 165/662] chore: delete Node 8 presubmit tests (#1004) --- .kokoro/presubmit/node8/browser-test.cfg | 12 ------------ .kokoro/presubmit/node8/common.cfg | 24 ------------------------ .kokoro/presubmit/node8/test.cfg | 0 3 files changed, 36 deletions(-) delete mode 100644 .kokoro/presubmit/node8/browser-test.cfg delete mode 100644 .kokoro/presubmit/node8/common.cfg delete mode 100644 .kokoro/presubmit/node8/test.cfg diff --git a/.kokoro/presubmit/node8/browser-test.cfg b/.kokoro/presubmit/node8/browser-test.cfg deleted file mode 100644 index fb5125fb..00000000 --- a/.kokoro/presubmit/node8/browser-test.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Download resources for system tests (service account key, etc.) -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:8-puppeteer" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/browser-test.sh" -} diff --git a/.kokoro/presubmit/node8/common.cfg b/.kokoro/presubmit/node8/common.cfg deleted file mode 100644 index e3a36c7f..00000000 --- a/.kokoro/presubmit/node8/common.cfg +++ /dev/null @@ -1,24 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:8-user" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/test.sh" -} diff --git a/.kokoro/presubmit/node8/test.cfg b/.kokoro/presubmit/node8/test.cfg deleted file mode 100644 index e69de29b..00000000 From a2b7d23caa5bf253c7c0756396f1b58216182089 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Wed, 15 Jul 2020 11:33:31 -0700 Subject: [PATCH 166/662] fix(types): include scope in credentials type (#1007) --- src/auth/credentials.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index a9ee2e19..a50499b2 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -13,19 +13,57 @@ // limitations under the License. export interface Credentials { + /** + * This field is only present if the access_type parameter was set to offline in the authentication request. For details, see Refresh tokens. + */ refresh_token?: string | null; + /** + * The time in ms at which this token is thought to expire. + */ expiry_date?: number | null; + /** + * A token that can be sent to a Google API. + */ access_token?: string | null; + /** + * Identifies the type of token returned. At this time, this field always has the value Bearer. + */ token_type?: string | null; + /** + * A JWT that contains identity information about the user that is digitally signed by Google. + */ id_token?: string | null; + /** + * The scopes of access granted by the access_token expressed as a list of space-delimited, case-sensitive strings. + */ + scope?: string; } export interface CredentialRequest { + /** + * This field is only present if the access_type parameter was set to offline in the authentication request. For details, see Refresh tokens. + */ refresh_token?: string; + /** + * A token that can be sent to a Google API. + */ access_token?: string; + /** + * Identifies the type of token returned. At this time, this field always has the value Bearer. + */ token_type?: string; + /** + * The remaining lifetime of the access token in seconds. + */ expires_in?: number; + /** + * A JWT that contains identity information about the user that is digitally signed by Google. + */ id_token?: string; + /** + * The scopes of access granted by the access_token expressed as a list of space-delimited, case-sensitive strings. + */ + scope?: string; } export interface JWTInput { From 72ec53c72528879b84b82a5f21bf15c4b9e50db6 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 17 Jul 2020 15:10:15 -0700 Subject: [PATCH 167/662] chore: add config files for cloud-rad for node.js, delete Node 8 templates (#1012) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/5e903fff-57bb-4395-bb94-8b4d1909dbf6/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/21f1470ecd01424dc91c70f1a7c798e4e87d1eec Source-Link: https://github.com/googleapis/synthtool/commit/388e10f5ae302d3e8de1fac99f3a95d1ab8f824a --- .kokoro/release/docs-devsite.cfg | 26 +++ .kokoro/release/docs-devsite.sh | 62 ++++++ api-extractor.json | 369 +++++++++++++++++++++++++++++++ synth.metadata | 4 +- 4 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 .kokoro/release/docs-devsite.cfg create mode 100755 .kokoro/release/docs-devsite.sh create mode 100644 api-extractor.json diff --git a/.kokoro/release/docs-devsite.cfg b/.kokoro/release/docs-devsite.cfg new file mode 100644 index 00000000..77a501f8 --- /dev/null +++ b/.kokoro/release/docs-devsite.cfg @@ -0,0 +1,26 @@ +# service account used to publish up-to-date docs. +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "docuploader_service_account" + } + } +} + +# doc publications use a Python image. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "nodejs-scheduler/.kokoro/trampoline.sh" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/nodejs-scheduler/.kokoro/release/docs-devsite.sh" +} diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh new file mode 100755 index 00000000..b679c48c --- /dev/null +++ b/.kokoro/release/docs-devsite.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# 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 +# +# https://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. + +set -eo pipefail + +# build jsdocs (Python is installed on the Node 10 docker image). +if [[ -z "$CREDENTIALS" ]]; then + # if CREDENTIALS are explicitly set, assume we're testing locally + # and don't set NPM_CONFIG_PREFIX. + export NPM_CONFIG_PREFIX=/home/node/.npm-global + export PATH="$PATH:/home/node/.npm-global/bin" + cd $(dirname $0)/../.. +fi + +mkdir ./etc + +npm install +npm run api-extractor +npm run api-documenter + +npm i json@9.0.6 -g +NAME=$(cat .repo-metadata.json | json name) + +mkdir ./_devsite +cp ./yaml/$NAME/* ./_devsite +cp ./yaml/toc.yml ./_devsite/_toc.yaml + +# create docs.metadata, based on package.json and .repo-metadata.json. +pip install -U pip +python3 -m pip install --user gcp-docuploader +python3 -m docuploader create-metadata \ + --name=$NAME \ + --version=$(cat package.json | json version) \ + --language=$(cat .repo-metadata.json | json language) \ + --distribution-name=$(cat .repo-metadata.json | json distribution_name) \ + --product-page=$(cat .repo-metadata.json | json product_documentation) \ + --github-repository=$(cat .repo-metadata.json | json repo) \ + --issue-tracker=$(cat .repo-metadata.json | json issue_tracker) +cp docs.metadata ./_devsite/docs.metadata + +# deploy the docs. +if [[ -z "$CREDENTIALS" ]]; then + CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account +fi +if [[ -z "$BUCKET" ]]; then + BUCKET=docs-staging-v2-staging +fi + +python3 -m docuploader upload ./_devsite --destination-prefix docfx --credentials $CREDENTIALS --staging-bucket $BUCKET diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 00000000..de228294 --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,369 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "/protos/protos.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [ ], + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true, + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/etc/" + */ + // "reportFolder": "/etc/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/" + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true, + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true, + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning", + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + }, + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning", + // "addToApiReportFile": false + }, + + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning", + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } + +} diff --git a/synth.metadata b/synth.metadata index 5bd58179..055cbcba 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "3ce427c75c7c92bd4f11954245d873ae98da75ab" + "sha": "a2b7d23caa5bf253c7c0756396f1b58216182089" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "d82deccf657a66e31bd5da9efdb96c6fa322fc7e" + "sha": "21f1470ecd01424dc91c70f1a7c798e4e87d1eec" } } ] From e9ec6995cd878a51f47f2a6a85d95d424bb7e0d0 Mon Sep 17 00:00:00 2001 From: "F. Hinkelmann" Date: Tue, 21 Jul 2020 14:40:13 -0400 Subject: [PATCH 168/662] chore: add dev dependencies for cloud-rad ref docs (#1014) --- package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 21e7c7fc..0a07fcb0 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,9 @@ "ts-loader": "^8.0.0", "typescript": "^3.8.3", "webpack": "^4.20.2", - "webpack-cli": "^3.1.1" + "webpack-cli": "^3.1.1", + "@microsoft/api-documenter": "^7.8.10", + "@microsoft/api-extractor": "^7.8.10" }, "files": [ "build/src", @@ -90,7 +92,9 @@ "docs-test": "linkinator docs", "predocs-test": "npm run docs", "prelint": "cd samples; npm link ../; npm install", - "precompile": "gts clean" + "precompile": "gts clean", + "api-extractor": "api-extractor run --local", + "api-documenter": "api-documenter yaml --input-folder=temp" }, "license": "Apache-2.0" } From 2dd42281c0830f82e11e5ada7dc4b2e130a28919 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 22 Jul 2020 17:40:43 -0700 Subject: [PATCH 169/662] build: rename _toc to toc (#1015) Source-Author: F. Hinkelmann Source-Date: Tue Jul 21 10:53:20 2020 -0400 Source-Repo: googleapis/synthtool Source-Sha: 99c93fe09f8c1dca09dfc0301c8668e3a70dd796 Source-Link: https://github.com/googleapis/synthtool/commit/99c93fe09f8c1dca09dfc0301c8668e3a70dd796 Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- .kokoro/release/docs-devsite.sh | 2 +- synth.metadata | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index b679c48c..3b93137d 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -36,7 +36,7 @@ NAME=$(cat .repo-metadata.json | json name) mkdir ./_devsite cp ./yaml/$NAME/* ./_devsite -cp ./yaml/toc.yml ./_devsite/_toc.yaml +cp ./yaml/toc.yml ./_devsite/toc.yml # create docs.metadata, based on package.json and .repo-metadata.json. pip install -U pip diff --git a/synth.metadata b/synth.metadata index 055cbcba..587502b3 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "a2b7d23caa5bf253c7c0756396f1b58216182089" + "sha": "72ec53c72528879b84b82a5f21bf15c4b9e50db6" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "21f1470ecd01424dc91c70f1a7c798e4e87d1eec" + "sha": "99c93fe09f8c1dca09dfc0301c8668e3a70dd796" } } ] From a292945146b95bc254aa5576db13536e27f35554 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 24 Jul 2020 10:12:56 -0700 Subject: [PATCH 170/662] build: move gitattributes files to node templates (#1017) Source-Author: F. Hinkelmann Source-Date: Thu Jul 23 01:45:04 2020 -0400 Source-Repo: googleapis/synthtool Source-Sha: 3a00b7fea8c4c83eaff8eb207f530a2e3e8e1de3 Source-Link: https://github.com/googleapis/synthtool/commit/3a00b7fea8c4c83eaff8eb207f530a2e3e8e1de3 --- .gitattributes | 1 + synth.metadata | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index d4f4169b..33739cb7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ *.ts text eol=lf *.js text eol=lf protos/* linguist-generated +**/api-extractor.json linguist-language=JSON-with-Comments diff --git a/synth.metadata b/synth.metadata index 587502b3..05bcc84f 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "72ec53c72528879b84b82a5f21bf15c4b9e50db6" + "sha": "2dd42281c0830f82e11e5ada7dc4b2e130a28919" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "99c93fe09f8c1dca09dfc0301c8668e3a70dd796" + "sha": "3a00b7fea8c4c83eaff8eb207f530a2e3e8e1de3" } } ] From 0dd91ace349c96e940d4e4ba0264e5817e4cc74a Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 29 Jul 2020 17:09:17 -0700 Subject: [PATCH 171/662] chore(node): fix kokoro build path for cloud-rad (#1018) Source-Author: F. Hinkelmann Source-Date: Wed Jul 29 00:28:42 2020 -0400 Source-Repo: googleapis/synthtool Source-Sha: 89d431fb2975fc4e0ed24995a6e6dfc8ff4c24fa Source-Link: https://github.com/googleapis/synthtool/commit/89d431fb2975fc4e0ed24995a6e6dfc8ff4c24fa --- .kokoro/release/docs-devsite.cfg | 4 ++-- synth.metadata | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.kokoro/release/docs-devsite.cfg b/.kokoro/release/docs-devsite.cfg index 77a501f8..906330c2 100644 --- a/.kokoro/release/docs-devsite.cfg +++ b/.kokoro/release/docs-devsite.cfg @@ -18,9 +18,9 @@ env_vars: { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "nodejs-scheduler/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/nodejs-scheduler/.kokoro/release/docs-devsite.sh" + value: "github/google-auth-library-nodejs/.kokoro/release/docs-devsite.sh" } diff --git a/synth.metadata b/synth.metadata index 05bcc84f..eaf9d3ce 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "2dd42281c0830f82e11e5ada7dc4b2e130a28919" + "sha": "a292945146b95bc254aa5576db13536e27f35554" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "3a00b7fea8c4c83eaff8eb207f530a2e3e8e1de3" + "sha": "89d431fb2975fc4e0ed24995a6e6dfc8ff4c24fa" } } ] From e2e840c96b40bc70fa5609596019d3dbb6c5ae87 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2020 17:18:22 -0700 Subject: [PATCH 172/662] chore: release 6.0.6 (#1009) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d27d6e..2f5efe0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.0.6](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.5...v6.0.6) (2020-07-30) + + +### Bug Fixes + +* **types:** include scope in credentials type ([#1007](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1007)) ([a2b7d23](https://www.github.com/googleapis/google-auth-library-nodejs/commit/a2b7d23caa5bf253c7c0756396f1b58216182089)) + ### [6.0.5](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.4...v6.0.5) (2020-07-13) diff --git a/package.json b/package.json index 0a07fcb0..a03bb699 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.5", + "version": "6.0.6", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 970b4075..56f31579 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.5", + "google-auth-library": "^6.0.6", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From a7e5701a8394d79fe93d28794467747a23cf9ff4 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Wed, 5 Aug 2020 10:04:43 -0700 Subject: [PATCH 173/662] fix: migrate token info API to not pass token in query string (#991) Google APIs will stop accepting requests that pass OAuth tokens on the query string from June 1, 2021. To align with security best practices, we should not pass the token in the query string when calling tokeninfo endpoint. This also follows the gcloud samples code: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/print-access-token?hl=en `curl -H "Content-Type: application/x-www-form-urlencoded" -d "access_token=$(gcloud auth application-default print-access-token)" https://www.googleapis.com/oauth2/v1/tokeninfo` --- src/auth/oauth2client.ts | 7 +++++-- test/test.oauth2.ts | 12 +++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 01edf9fc..9719212c 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -1015,9 +1015,12 @@ export class OAuth2Client extends AuthClient { */ async getTokenInfo(accessToken: string): Promise { const {data} = await this.transporter.request({ - method: 'GET', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, url: OAuth2Client.GOOGLE_TOKEN_INFO_URL, - params: {access_token: accessToken}, + data: querystring.stringify({access_token: accessToken}), }); const info = Object.assign( { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index dffa899e..e53fbcaa 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1323,7 +1323,17 @@ describe('oauth2', () => { }; const scope = nock(baseUrl) - .get(`/tokeninfo?access_token=${accessToken}`) + .post( + '/tokeninfo', + qs.stringify({ + access_token: accessToken, + }), + { + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded', + }, + } + ) .reply(200, tokenInfo); const info = await client.getTokenInfo(accessToken); From 11b0cb735c47cecef939b0af934747cf9cd29e9a Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Fri, 7 Aug 2020 16:32:13 -0700 Subject: [PATCH 174/662] refactor(samples): idtoken-cloudrun => idtoken-serverless (#1025) --- README.md | 12 ++++++---- samples/README.md | 24 ++++++++++++++++--- ...ens-cloudrun.js => idtokens-serverless.js} | 22 +++++++++-------- samples/test/jwt.test.js | 9 ++++--- 4 files changed, 45 insertions(+), 22 deletions(-) rename samples/{idtokens-cloudrun.js => idtokens-serverless.js} (71%) diff --git a/README.md b/README.md index fdaa7381..0c2d03cf 100644 --- a/README.md +++ b/README.md @@ -335,7 +335,7 @@ main().catch(console.error); ## Working with ID Tokens ### Fetching ID Tokens -If your application is running behind Cloud Run, or using Cloud Identity-Aware +If your application is running on Cloud Run or Cloud Functions, or using Cloud Identity-Aware Proxy (IAP), you will need to fetch an ID token to access your application. For this, use the method `getIdTokenClient` on the `GoogleAuth` client. @@ -343,12 +343,16 @@ For invoking Cloud Run services, your service account will need the [`Cloud Run Invoker`](https://cloud.google.com/run/docs/authenticating/service-to-service) IAM permission. +For invoking Cloud Functions, your service account will need the +[`Function Invoker`](https://cloud.google.com/functions/docs/securing/authenticating#function-to-function) +IAM permission. + ``` js -// Make a request to a protected Cloud Run +// Make a request to a protected Cloud Run service. const {GoogleAuth} = require('google-auth-library'); async function main() { - const url = 'https://cloud-run-url.com'; + const url = 'https://cloud-run-1234-uc.a.run.app'; const auth = new GoogleAuth(); const client = auth.getIdTokenClient(url); const res = await client.request({url}); @@ -358,7 +362,7 @@ async function main() { main().catch(console.error); ``` -A complete example can be found in [`samples/idtokens-cloudrun.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-cloudrun.js). +A complete example can be found in [`samples/idtokens-serverless.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). For invoking Cloud Identity-Aware Proxy, you will need to pass the Client ID used when you set up your protected resource as the target audience. diff --git a/samples/README.md b/samples/README.md index 9e6f6298..49711aec 100644 --- a/samples/README.md +++ b/samples/README.md @@ -110,18 +110,36 @@ __Usage:__ +### ID Tokens for Cloud Functions + +Requests a Cloud Functions URL with an ID Token. + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) + +__Usage:__ + + +`node idtokens-serverless.js []` + + +----- + + + ### ID Tokens for Cloud Run Requests a Cloud Run URL with an ID Token. -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-cloudrun.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). -[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-cloudrun.js,samples/README.md) +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) __Usage:__ -`node idtokens-cloudrun.js []` +`node idtokens-serverless.js []` ----- diff --git a/samples/idtokens-cloudrun.js b/samples/idtokens-serverless.js similarity index 71% rename from samples/idtokens-cloudrun.js rename to samples/idtokens-serverless.js index 213f3567..c76a8605 100644 --- a/samples/idtokens-cloudrun.js +++ b/samples/idtokens-serverless.js @@ -12,9 +12,9 @@ // limitations under the License. // sample-metadata: -// title: ID Tokens for Cloud Run -// description: Requests a Cloud Run URL with an ID Token. -// usage: node idtokens-cloudrun.js [] +// title: ID Tokens for Serverless +// description: Requests a Cloud Run or Cloud Functions URL with an ID Token. +// usage: node idtokens-serverless.js [] 'use strict'; @@ -22,23 +22,23 @@ function main( url = 'https://service-1234-uc.a.run.app', targetAudience = null ) { - // [START google_auth_idtoken_cloudrun] + // [START google_auth_idtoken_serverless] + // [START run_service_to_service_auth] + // [START functions_bearer_token] /** * TODO(developer): Uncomment these variables before running the sample. */ - // const url = 'https://YOUR_CLOUD_RUN_URL.run.app'; + // const url = 'https://TARGET_URL'; const {GoogleAuth} = require('google-auth-library'); const auth = new GoogleAuth(); async function request() { if (!targetAudience) { - // Use the request URL hostname as the target audience for Cloud Run requests + // Use the request URL hostname as the target audience for requests. const {URL} = require('url'); targetAudience = new URL(url).origin; } - console.info( - `request Cloud Run ${url} with target audience ${targetAudience}` - ); + console.info(`request ${url} with target audience ${targetAudience}`); const client = await auth.getIdTokenClient(targetAudience); const res = await client.request({url}); console.info(res.data); @@ -48,7 +48,9 @@ function main( console.error(err.message); process.exitCode = 1; }); - // [END google_auth_idtoken_cloudrun] + // [END functions_bearer_token] + // [END run_service_to_service_auth] + // [END google_auth_idtoken_serverless] } const args = process.argv.slice(2); diff --git a/samples/test/jwt.test.js b/samples/test/jwt.test.js index 42c2b226..e87792bd 100644 --- a/samples/test/jwt.test.js +++ b/samples/test/jwt.test.js @@ -65,17 +65,16 @@ describe('samples', () => { }); it('should fetch ID token for Cloud Run', async () => { - // process.env.CLOUD_RUN_URL should be a cloud run container, protected with - // IAP, running gcr.io/cloudrun/hello: + // process.env.CLOUD_RUN_URL should be a cloud run service running + // gcr.io/cloudrun/hello: const url = process.env.CLOUD_RUN_URL || 'https://hello-rftcw63abq-uc.a.run.app'; - const output = execSync(`node idtokens-cloudrun ${url}`); + const output = execSync(`node idtokens-serverless ${url}`); assert.match(output, /What's next?/); }); it('should fetch ID token for IAP', async () => { - // process.env.CLOUD_RUN_URL should be a cloud run container, protected with - // IAP, running gcr.io/cloudrun/hello: + // process.env.IAP_URL should be an App Engine app, protected with IAP: const url = process.env.IAP_URL || 'https://nodejs-docs-samples-iap.appspot.com'; const targetAudience = From 4df48dff431fd49f0270c7e3509c3c5874d7a9b5 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 10 Aug 2020 18:42:52 -0700 Subject: [PATCH 175/662] docs: README updated with new sample paths --- samples/README.md | 36 +++++++++--------------------------- synth.metadata | 2 +- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/samples/README.md b/samples/README.md index 49711aec..40571df3 100644 --- a/samples/README.md +++ b/samples/README.md @@ -16,8 +16,8 @@ * [Compute](#compute) * [Credentials](#credentials) * [Headers](#headers) - * [ID Tokens for Cloud Run](#id-tokens-for-cloud-run) * [ID Tokens for Identity-Aware Proxy (IAP)](#id-tokens-for-identity-aware-proxy-iap) + * [ID Tokens for Serverless](#id-tokens-for-serverless) * [Jwt](#jwt) * [Keepalive](#keepalive) * [Keyfile](#keyfile) @@ -110,27 +110,28 @@ __Usage:__ -### ID Tokens for Cloud Functions +### ID Tokens for Identity-Aware Proxy (IAP) -Requests a Cloud Functions URL with an ID Token. +Requests an IAP-protected resource with an ID Token. -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). -[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) __Usage:__ -`node idtokens-serverless.js []` +`node idtokens-iap.js ` ----- -### ID Tokens for Cloud Run -Requests a Cloud Run URL with an ID Token. +### ID Tokens for Serverless + +Requests a Cloud Run or Cloud Functions URL with an ID Token. View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). @@ -147,25 +148,6 @@ __Usage:__ -### ID Tokens for Identity-Aware Proxy (IAP) - -Requests an IAP-protected resource with an ID Token. - -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). - -[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) - -__Usage:__ - - -`node idtokens-iap.js ` - - ------ - - - - ### Jwt View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/jwt.js). diff --git a/synth.metadata b/synth.metadata index eaf9d3ce..4dcb2a4b 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "a292945146b95bc254aa5576db13536e27f35554" + "sha": "11b0cb735c47cecef939b0af934747cf9cd29e9a" } }, { From 6dfee1d51e0c2d623bcb5bf646750e2fb7792538 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2020 21:53:22 -0700 Subject: [PATCH 176/662] chore: release 6.0.7 (#1023) * chore: updated samples/package.json [ci skip] * chore: updated CHANGELOG.md [ci skip] * chore: updated package.json Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5efe0e..989e781a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.0.7](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.6...v6.0.7) (2020-08-11) + + +### Bug Fixes + +* migrate token info API to not pass token in query string ([#991](https://www.github.com/googleapis/google-auth-library-nodejs/issues/991)) ([a7e5701](https://www.github.com/googleapis/google-auth-library-nodejs/commit/a7e5701a8394d79fe93d28794467747a23cf9ff4)) + ### [6.0.6](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.5...v6.0.6) (2020-07-30) diff --git a/package.json b/package.json index a03bb699..836568b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.6", + "version": "6.0.7", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 56f31579..448bd8d6 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.6", + "google-auth-library": "^6.0.7", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From b976c8aabac409e96aadaf3a515a03c5da8eda29 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 11 Aug 2020 22:22:06 -0700 Subject: [PATCH 177/662] build: credential-file-override is no longer required (#1029) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/5f7f9c6d-c75a-4c60-8bb8-0026a14cead7/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/94421c47802f56a44c320257b2b4c190dc7d6b68 --- .kokoro/populate-secrets.sh | 1 - synth.metadata | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh index e6ce8200..6f9d2288 100755 --- a/.kokoro/populate-secrets.sh +++ b/.kokoro/populate-secrets.sh @@ -32,7 +32,6 @@ do --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \ gcr.io/google.com/cloudsdktool/cloud-sdk \ secrets versions access latest \ - --credential-file-override=${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json \ --project cloud-devrel-kokoro-resources \ --secret $key > \ "$SECRET_LOCATION/$key" diff --git a/synth.metadata b/synth.metadata index 4dcb2a4b..bd4f99d5 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "11b0cb735c47cecef939b0af934747cf9cd29e9a" + "sha": "4df48dff431fd49f0270c7e3509c3c5874d7a9b5" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "89d431fb2975fc4e0ed24995a6e6dfc8ff4c24fa" + "sha": "94421c47802f56a44c320257b2b4c190dc7d6b68" } } ] From 4830a5308d780822e884b0fb98fa605d3e7dc77b Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 12 Aug 2020 11:06:06 -0700 Subject: [PATCH 178/662] chore: update cloud rad kokoro build job (#1034) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/b742586e-df31-4aac-8092-78288e9ea8e7/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/bd0deaa1113b588d70449535ab9cbf0f2bd0e72f --- .kokoro/release/docs-devsite.sh | 5 +++++ synth.metadata | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index 3b93137d..fa089cf2 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -36,6 +36,11 @@ NAME=$(cat .repo-metadata.json | json name) mkdir ./_devsite cp ./yaml/$NAME/* ./_devsite + +# Delete SharePoint item, see https://github.com/microsoft/rushstack/issues/1229 +sed -i -e '1,3d' ./yaml/toc.yml +sed -i -e 's/^ //' ./yaml/toc.yml + cp ./yaml/toc.yml ./_devsite/toc.yml # create docs.metadata, based on package.json and .repo-metadata.json. diff --git a/synth.metadata b/synth.metadata index bd4f99d5..98567439 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "4df48dff431fd49f0270c7e3509c3c5874d7a9b5" + "sha": "b976c8aabac409e96aadaf3a515a03c5da8eda29" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "94421c47802f56a44c320257b2b4c190dc7d6b68" + "sha": "bd0deaa1113b588d70449535ab9cbf0f2bd0e72f" } } ] From eb54ee9369d9e5a01d164ccf7f826858d44827fd Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 12 Aug 2020 23:04:25 +0200 Subject: [PATCH 179/662] fix(deps): roll back dependency google-auth-library to ^6.0.6 (#1033) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 448bd8d6..56f31579 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.7", + "google-auth-library": "^6.0.6", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 714247d673f8fbbb00cb8886c5f795a4a2d51440 Mon Sep 17 00:00:00 2001 From: Jeffrey Rennie Date: Thu, 13 Aug 2020 09:52:04 -0700 Subject: [PATCH 180/662] build: perform publish using Node 12 (#1035) This PR was generated using Autosynth. :rainbow: - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/5747555f7620113d9a2078a48f4c047a99d31b3e --- .kokoro/release/publish.cfg | 2 +- synth.metadata | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index 3bed518a..e63ee55f 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -61,7 +61,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:8-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" } env_vars: { diff --git a/synth.metadata b/synth.metadata index 98567439..0d36d211 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "b976c8aabac409e96aadaf3a515a03c5da8eda29" + "sha": "4830a5308d780822e884b0fb98fa605d3e7dc77b" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "bd0deaa1113b588d70449535ab9cbf0f2bd0e72f" + "sha": "5747555f7620113d9a2078a48f4c047a99d31b3e" } } ] From 211e0427affdf8c5c9537faaa3d487e1045f3672 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2020 10:17:57 -0700 Subject: [PATCH 181/662] chore: release 6.0.8 (#1036) * chore: updated samples/package.json [ci skip] * chore: updated CHANGELOG.md [ci skip] * chore: updated package.json Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 989e781a..daba9d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.0.8](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.7...v6.0.8) (2020-08-13) + + +### Bug Fixes + +* **deps:** roll back dependency google-auth-library to ^6.0.6 ([#1033](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1033)) ([eb54ee9](https://www.github.com/googleapis/google-auth-library-nodejs/commit/eb54ee9369d9e5a01d164ccf7f826858d44827fd)) + ### [6.0.7](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.6...v6.0.7) (2020-08-11) diff --git a/package.json b/package.json index 836568b0..f5f04166 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.7", + "version": "6.0.8", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 56f31579..57e6e542 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.6", + "google-auth-library": "^6.0.8", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From d4e56c0937adbf9561d5ca3860a1bde623696db7 Mon Sep 17 00:00:00 2001 From: xethlyx <46338199+xethlyx@users.noreply.github.com> Date: Mon, 17 Aug 2020 20:14:05 -0400 Subject: [PATCH 182/662] docs: correct spelling of its (#1040) Minor change that fixes a spelling error. I don't think an issue needed to be opened for this. Thanks! --- src/auth/oauth2client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 9719212c..046863f5 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -545,7 +545,7 @@ export class OAuth2Client extends AuthClient { } /** - * Convenience method to automatically generate a code_verifier, and it's + * Convenience method to automatically generate a code_verifier, and its * resulting SHA256. If used, this must be paired with a S256 * code_challenge_method. * From 6c1513997cef3fdefe341977a0683a614e1694de Mon Sep 17 00:00:00 2001 From: Kohei Hasegawa Date: Wed, 19 Aug 2020 23:51:16 +0900 Subject: [PATCH 183/662] docs: fix usage in readme (#1031) Add await Co-authored-by: Justin Beckwith Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> Co-authored-by: Benjamin E. Coe --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c2d03cf..f29c488a 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ const {GoogleAuth} = require('google-auth-library'); async function main() { const url = 'https://cloud-run-1234-uc.a.run.app'; const auth = new GoogleAuth(); - const client = auth.getIdTokenClient(url); + const client = await auth.getIdTokenClient(url); const res = await client.request({url}); console.log(res.data); } @@ -375,7 +375,7 @@ async function main() const targetAudience = 'iap-client-id'; const url = 'https://iap-url.com'; const auth = new GoogleAuth(); - const client = auth.getIdTokenClient(targetAudience); + const client = await auth.getIdTokenClient(targetAudience); const res = await client.request({url}); console.log(res.data); } From 0c8e086f3cad23efefb57418f1eeccba6674aaac Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 19 Aug 2020 10:06:04 -0700 Subject: [PATCH 184/662] chore: start tracking obsolete files (#1043) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/7a1b0b96-8ddb-4836-a1a2-d2f73b7e6ffe/targets - [ ] To automatically regenerate this PR, check this box. --- synth.metadata | 57 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/synth.metadata b/synth.metadata index 0d36d211..b517ab41 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,15 +4,68 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "4830a5308d780822e884b0fb98fa605d3e7dc77b" + "sha": "d4e56c0937adbf9561d5ca3860a1bde623696db7" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "5747555f7620113d9a2078a48f4c047a99d31b3e" + "sha": "1a60ff2a3975c2f5054431588bd95db9c3b862ba" } } + ], + "generatedFiles": [ + ".eslintignore", + ".eslintrc.json", + ".gitattributes", + ".github/ISSUE_TEMPLATE/bug_report.md", + ".github/ISSUE_TEMPLATE/feature_request.md", + ".github/ISSUE_TEMPLATE/support_request.md", + ".github/PULL_REQUEST_TEMPLATE.md", + ".github/publish.yml", + ".github/release-please.yml", + ".github/workflows/ci.yaml", + ".kokoro/.gitattributes", + ".kokoro/common.cfg", + ".kokoro/continuous/node10/common.cfg", + ".kokoro/continuous/node10/docs.cfg", + ".kokoro/continuous/node10/lint.cfg", + ".kokoro/continuous/node10/samples-test.cfg", + ".kokoro/continuous/node10/system-test.cfg", + ".kokoro/continuous/node10/test.cfg", + ".kokoro/continuous/node12/common.cfg", + ".kokoro/continuous/node12/test.cfg", + ".kokoro/docs.sh", + ".kokoro/lint.sh", + ".kokoro/populate-secrets.sh", + ".kokoro/presubmit/node10/common.cfg", + ".kokoro/presubmit/node10/samples-test.cfg", + ".kokoro/presubmit/node10/system-test.cfg", + ".kokoro/presubmit/node12/common.cfg", + ".kokoro/presubmit/node12/test.cfg", + ".kokoro/publish.sh", + ".kokoro/release/docs-devsite.cfg", + ".kokoro/release/docs-devsite.sh", + ".kokoro/release/docs.cfg", + ".kokoro/release/docs.sh", + ".kokoro/release/publish.cfg", + ".kokoro/samples-test.sh", + ".kokoro/system-test.sh", + ".kokoro/test.bat", + ".kokoro/test.sh", + ".kokoro/trampoline.sh", + ".mocharc.js", + ".nycrc", + ".prettierignore", + ".prettierrc.js", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "LICENSE", + "api-extractor.json", + "package-lock.json.1130524823", + "renovate.json", + "samples/README.md", + "samples/package-lock.json.615092575" ] } \ No newline at end of file From 5164845bbeb46733a56e0d2c25d6c205e671fe3c Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 21 Aug 2020 09:28:16 -0700 Subject: [PATCH 185/662] build: move system and samples test from Node 10 to Node 12 (#1045) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/ba2d388f-b3b2-4ad7-a163-0c6b4d86894f/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/05de3e1e14a0b07eab8b474e669164dbd31f81fb --- .kokoro/continuous/{node10 => node12}/lint.cfg | 0 .../{node10 => node12}/samples-test.cfg | 0 .../{node10 => node12}/system-test.cfg | 0 .../{node10 => node12}/samples-test.cfg | 0 .../{node10 => node12}/system-test.cfg | 0 synth.metadata | 18 ++++++++---------- 6 files changed, 8 insertions(+), 10 deletions(-) rename .kokoro/continuous/{node10 => node12}/lint.cfg (100%) rename .kokoro/continuous/{node10 => node12}/samples-test.cfg (100%) rename .kokoro/continuous/{node10 => node12}/system-test.cfg (100%) rename .kokoro/presubmit/{node10 => node12}/samples-test.cfg (100%) rename .kokoro/presubmit/{node10 => node12}/system-test.cfg (100%) diff --git a/.kokoro/continuous/node10/lint.cfg b/.kokoro/continuous/node12/lint.cfg similarity index 100% rename from .kokoro/continuous/node10/lint.cfg rename to .kokoro/continuous/node12/lint.cfg diff --git a/.kokoro/continuous/node10/samples-test.cfg b/.kokoro/continuous/node12/samples-test.cfg similarity index 100% rename from .kokoro/continuous/node10/samples-test.cfg rename to .kokoro/continuous/node12/samples-test.cfg diff --git a/.kokoro/continuous/node10/system-test.cfg b/.kokoro/continuous/node12/system-test.cfg similarity index 100% rename from .kokoro/continuous/node10/system-test.cfg rename to .kokoro/continuous/node12/system-test.cfg diff --git a/.kokoro/presubmit/node10/samples-test.cfg b/.kokoro/presubmit/node12/samples-test.cfg similarity index 100% rename from .kokoro/presubmit/node10/samples-test.cfg rename to .kokoro/presubmit/node12/samples-test.cfg diff --git a/.kokoro/presubmit/node10/system-test.cfg b/.kokoro/presubmit/node12/system-test.cfg similarity index 100% rename from .kokoro/presubmit/node10/system-test.cfg rename to .kokoro/presubmit/node12/system-test.cfg diff --git a/synth.metadata b/synth.metadata index b517ab41..9107ca41 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "d4e56c0937adbf9561d5ca3860a1bde623696db7" + "sha": "0c8e086f3cad23efefb57418f1eeccba6674aaac" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "1a60ff2a3975c2f5054431588bd95db9c3b862ba" + "sha": "05de3e1e14a0b07eab8b474e669164dbd31f81fb" } } ], @@ -30,19 +30,19 @@ ".kokoro/common.cfg", ".kokoro/continuous/node10/common.cfg", ".kokoro/continuous/node10/docs.cfg", - ".kokoro/continuous/node10/lint.cfg", - ".kokoro/continuous/node10/samples-test.cfg", - ".kokoro/continuous/node10/system-test.cfg", ".kokoro/continuous/node10/test.cfg", ".kokoro/continuous/node12/common.cfg", + ".kokoro/continuous/node12/lint.cfg", + ".kokoro/continuous/node12/samples-test.cfg", + ".kokoro/continuous/node12/system-test.cfg", ".kokoro/continuous/node12/test.cfg", ".kokoro/docs.sh", ".kokoro/lint.sh", ".kokoro/populate-secrets.sh", ".kokoro/presubmit/node10/common.cfg", - ".kokoro/presubmit/node10/samples-test.cfg", - ".kokoro/presubmit/node10/system-test.cfg", ".kokoro/presubmit/node12/common.cfg", + ".kokoro/presubmit/node12/samples-test.cfg", + ".kokoro/presubmit/node12/system-test.cfg", ".kokoro/presubmit/node12/test.cfg", ".kokoro/publish.sh", ".kokoro/release/docs-devsite.cfg", @@ -63,9 +63,7 @@ "CONTRIBUTING.md", "LICENSE", "api-extractor.json", - "package-lock.json.1130524823", "renovate.json", - "samples/README.md", - "samples/package-lock.json.615092575" + "samples/README.md" ] } \ No newline at end of file From 55b98171efffc2e7dfecd46ff3485382c3ab62f5 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 27 Aug 2020 09:26:09 -0700 Subject: [PATCH 186/662] build: track flaky tests for "nightly", add new secrets for tagging (#1048) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/96acae41-dfd7-4d71-95d3-12436053b826/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/8cf6d2834ad14318e64429c3b94f6443ae83daf9 --- .github/publish.yml | 0 .kokoro/release/publish.cfg | 2 +- .kokoro/samples-test.sh | 2 +- .kokoro/system-test.sh | 2 +- .kokoro/test.sh | 2 +- synth.metadata | 5 ++--- 6 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 .github/publish.yml diff --git a/.github/publish.yml b/.github/publish.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index e63ee55f..a70341d4 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -49,7 +49,7 @@ before_action { env_vars: { key: "SECRET_MANAGER_KEYS" - value: "npm_publish_token" + value: "npm_publish_token,releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" } # Download trampoline resources. diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index 86e83c9d..c0c40139 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -41,7 +41,7 @@ if [ -f samples/package.json ]; then cd .. # If tests are running against master, configure Build Cop # to open issues on failures: - if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then + if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml export MOCHA_REPORTER=xunit cleanup() { diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index dfae142a..283f1700 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -35,7 +35,7 @@ npm install # If tests are running against master, configure Build Cop # to open issues on failures: -if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then +if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml export MOCHA_REPORTER=xunit cleanup() { diff --git a/.kokoro/test.sh b/.kokoro/test.sh index 8d9c2954..47be59b9 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -23,7 +23,7 @@ cd $(dirname $0)/.. npm install # If tests are running against master, configure Build Cop # to open issues on failures: -if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then +if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml export MOCHA_REPORTER=xunit cleanup() { diff --git a/synth.metadata b/synth.metadata index 9107ca41..f3345524 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "0c8e086f3cad23efefb57418f1eeccba6674aaac" + "sha": "5164845bbeb46733a56e0d2c25d6c205e671fe3c" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "05de3e1e14a0b07eab8b474e669164dbd31f81fb" + "sha": "8cf6d2834ad14318e64429c3b94f6443ae83daf9" } } ], @@ -23,7 +23,6 @@ ".github/ISSUE_TEMPLATE/feature_request.md", ".github/ISSUE_TEMPLATE/support_request.md", ".github/PULL_REQUEST_TEMPLATE.md", - ".github/publish.yml", ".github/release-please.yml", ".github/workflows/ci.yaml", ".kokoro/.gitattributes", From d835af79dc6c7b75c5ac4f8d252cfcb3491fd7ce Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Sat, 12 Sep 2020 14:06:06 -0700 Subject: [PATCH 187/662] build(test): recursively find test files; fail on unsupported dependency versions (#1055) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/57acd272-496f-4414-af01-fc62837d5aa1/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/fdd03c161003ab97657cc0218f25c82c89ddf4b6 --- .github/workflows/ci.yaml | 2 +- .mocharc.js | 3 ++- synth.metadata | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5e73bb3d..7dd110e3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: with: node-version: ${{ matrix.node }} - run: node --version - - run: npm install + - run: npm install --engine-strict - run: npm test - name: coverage uses: codecov/codecov-action@v1 diff --git a/.mocharc.js b/.mocharc.js index ff7b34fa..0b600509 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -14,7 +14,8 @@ const config = { "enable-source-maps": true, "throw-deprecation": true, - "timeout": 10000 + "timeout": 10000, + "recursive": true } if (process.env.MOCHA_THROW_DEPRECATION === 'false') { delete config['throw-deprecation']; diff --git a/synth.metadata b/synth.metadata index f3345524..1da5b692 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "5164845bbeb46733a56e0d2c25d6c205e671fe3c" + "sha": "55b98171efffc2e7dfecd46ff3485382c3ab62f5" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "8cf6d2834ad14318e64429c3b94f6443ae83daf9" + "sha": "fdd03c161003ab97657cc0218f25c82c89ddf4b6" } } ], From b4d139d9ee27f886ca8cc5478615c052700fff48 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 22 Sep 2020 10:29:04 -0700 Subject: [PATCH 188/662] feat: default self-signed JWTs (#1054) --- src/auth/googleauth.ts | 12 ++++- src/auth/jwtaccess.ts | 41 +++++++++++--- src/auth/jwtclient.ts | 38 ++++++++----- test/test.jwt.ts | 110 ++++++++++++++++++++++++++++++++++++++ test/test.transporters.ts | 2 - 5 files changed, 181 insertions(+), 22 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index fb5065e0..29ff5d95 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -119,6 +119,12 @@ export class GoogleAuth { cachedCredential: JWT | UserRefreshClient | Compute | null = null; + /** + * Scopes populated by the client library by default. We differentiate between + * these and user defined scopes when deciding whether to use a self-signed JWT. + */ + defaultScopes?: string | string[]; + private keyFilename?: string; private scopes?: string | string[]; private clientOptions?: RefreshOptions; @@ -244,6 +250,7 @@ export class GoogleAuth { ); if (credential) { if (credential instanceof JWT) { + credential.defaultScopes = this.defaultScopes; credential.scopes = this.scopes; } this.cachedCredential = credential; @@ -257,6 +264,7 @@ export class GoogleAuth { ); if (credential) { if (credential instanceof JWT) { + credential.defaultScopes = this.defaultScopes; credential.scopes = this.scopes; } this.cachedCredential = credential; @@ -282,7 +290,7 @@ export class GoogleAuth { // For GCE, just return a default ComputeClient. It will take care of // the rest. - (options as ComputeOptions).scopes = this.scopes; + (options as ComputeOptions).scopes = this.scopes || this.defaultScopes; this.cachedCredential = new Compute(options); projectId = await this.getProjectId(); return {projectId, credential: this.cachedCredential}; @@ -422,6 +430,7 @@ export class GoogleAuth { } else { (options as JWTOptions).scopes = this.scopes; client = new JWT(options); + client.defaultScopes = this.defaultScopes; } client.fromJSON(json); return client; @@ -446,6 +455,7 @@ export class GoogleAuth { } else { (options as JWTOptions).scopes = this.scopes; client = new JWT(options); + client.defaultScopes = this.defaultScopes; } client.fromJSON(json); // cache both raw data used to instantiate client and client itself. diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index 0139683e..30298b6c 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -33,8 +33,12 @@ export class JWTAccess { key?: string | null; keyId?: string | null; projectId?: string; + eagerRefreshThresholdMillis: number; - private cache = new LRU({max: 500, maxAge: 60 * 60 * 1000}); + private cache = new LRU({ + max: 500, + maxAge: 60 * 60 * 1000, + }); /** * JWTAccess service account credentials. @@ -49,11 +53,14 @@ export class JWTAccess { constructor( email?: string | null, key?: string | null, - keyId?: string | null + keyId?: string | null, + eagerRefreshThresholdMillis?: number ) { this.email = email; this.key = key; this.keyId = keyId; + this.eagerRefreshThresholdMillis = + eagerRefreshThresholdMillis ?? 5 * 60 * 1000; } /** @@ -65,12 +72,18 @@ export class JWTAccess { * @returns An object that includes the authorization header. */ getRequestHeaders(url: string, additionalClaims?: Claims): Headers { + // Return cached authorization headers, unless we are within + // eagerRefreshThresholdMillis ms of them expiring: const cachedToken = this.cache.get(url); - if (cachedToken) { - return cachedToken; + const now = Date.now(); + if ( + cachedToken && + cachedToken.expiration - now > this.eagerRefreshThresholdMillis + ) { + return cachedToken.headers; } - const iat = Math.floor(new Date().getTime() / 1000); - const exp = iat + 3600; // 3600 seconds = 1 hour + const iat = Math.floor(Date.now() / 1000); + const exp = JWTAccess.getExpirationTime(iat); // The payload used for signed JWT headers has: // iss == sub == @@ -103,10 +116,24 @@ export class JWTAccess { // Sign the jwt and add it to the cache const signedJWT = jws.sign({header, payload, secret: this.key}); const headers = {Authorization: `Bearer ${signedJWT}`}; - this.cache.set(url, headers); + this.cache.set(url, { + expiration: exp * 1000, + headers, + }); return headers; } + /** + * Returns an expiration time for the JWT token. + * + * @param iat The issued at time for the JWT. + * @returns An expiration time for the JWT. + */ + private static getExpirationTime(iat: number): number { + const exp = iat + 3600; // 3600 seconds = 1 hour + return exp; + } + /** * Create a JWTAccess credentials instance using the given input options. * @param json The input object. diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index c203cbb5..344aff62 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -40,6 +40,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { keyFile?: string; key?: string; keyId?: string; + defaultScopes?: string | string[]; scopes?: string | string[]; scope?: string; subject?: string; @@ -120,7 +121,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { protected async getRequestMetadataAsync( url?: string | null ): Promise { - if (!this.apiKey && !this.hasScopes() && url) { + if (!this.apiKey && !this.hasUserScopes() && url) { if ( this.additionalClaims && (this.additionalClaims as { @@ -137,7 +138,12 @@ export class JWT extends OAuth2Client implements IdTokenProvider { // no scopes have been set, but a uri has been provided. Use JWTAccess // credentials. if (!this.access) { - this.access = new JWTAccess(this.email, this.key, this.keyId); + this.access = new JWTAccess( + this.email, + this.key, + this.keyId, + this.eagerRefreshThresholdMillis + ); } const headers = await this.access.getRequestHeaders( url, @@ -145,8 +151,12 @@ export class JWT extends OAuth2Client implements IdTokenProvider { ); return {headers: this.addSharedMetadataHeaders(headers)}; } - } else { + } else if (this.hasAnyScopes() || this.apiKey) { return super.getRequestMetadataAsync(url); + } else { + // If no audience, apiKey, or scopes are provided, we should not attempt + // to populate any headers: + return {headers: {}}; } } @@ -159,7 +169,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { const gtoken = new GoogleToken({ iss: this.email, sub: this.subject, - scope: this.scopes, + scope: this.scopes || this.defaultScopes, keyFile: this.keyFile, key: this.key, additionalClaims: {target_audience: targetAudience}, @@ -176,16 +186,20 @@ export class JWT extends OAuth2Client implements IdTokenProvider { /** * Determine if there are currently scopes available. */ - private hasScopes() { + private hasUserScopes() { if (!this.scopes) { return false; } - // For arrays, check the array length. - if (this.scopes instanceof Array) { - return this.scopes.length > 0; - } - // For others, convert to a string and check the length. - return String(this.scopes).length > 0; + return this.scopes.length > 0; + } + + /** + * Are there any default or user scopes defined. + */ + private hasAnyScopes() { + if (this.scopes && this.scopes.length > 0) return true; + if (this.defaultScopes && this.defaultScopes.length > 0) return true; + return false; } /** @@ -248,7 +262,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { this.gtoken = new GoogleToken({ iss: this.email, sub: this.subject, - scope: this.scopes, + scope: this.scopes || this.defaultScopes, keyFile: this.keyFile, key: this.key, additionalClaims: this.additionalClaims, diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 14b40477..dd569a03 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -21,6 +21,7 @@ import * as sinon from 'sinon'; import {GoogleAuth, JWT} from '../src'; import {CredentialRequest, JWTInput} from '../src/auth/credentials'; +import * as jwtaccess from '../src/auth/jwtaccess'; describe('jwt', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -783,4 +784,113 @@ describe('jwt', () => { } assert.fail('failed to throw'); }); + + describe('self-signed JWT', () => { + afterEach(() => { + sandbox.restore(); + }); + + it('uses self signed JWT when no scopes are provided', async () => { + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: sinon.stub().returns({}), + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + }); + + it('uses self signed JWT when default scopes are provided', async () => { + const JWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: sinon.stub().returns({}), + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + subject: 'bar@subjectaccount.com', + }); + jwt.defaultScopes = ['http://bar', 'http://foo']; + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(JWTAccess); + }); + + it('does not use self signed JWT if target_audience provided', async () => { + const JWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: sinon.stub().returns({}), + }); + const keys = keypair(512 /* bitsize of private key */); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: keys.private, + subject: 'ignored@subjectaccount.com', + additionalClaims: {target_audience: 'beepboop'}, + }); + jwt.defaultScopes = ['foo', 'bar']; + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + const testUri = 'http:/example.com/my_test_service'; + const scope = createGTokenMock({id_token: 'abc123'}); + await jwt.getRequestHeaders(testUri); + scope.done(); + sandbox.assert.notCalled(JWTAccess); + }); + + it('returns headers from cache, prior to their expiry time', async () => { + const sign = sandbox.stub(jws, 'sign').returns('abc123'); + const getExpirationTime = sandbox + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .stub(jwtaccess.JWTAccess as any, 'getExpirationTime') + .returns(Date.now() / 1000 + 3600); // expire in an hour. + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + // The second time we fetch headers should not cause getExpirationTime + // to be invoked a second time: + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(getExpirationTime); + sandbox.assert.calledOnce(sign); + }); + + it('creates a new self-signed JWT, if headers are close to expiring', async () => { + const sign = sandbox.stub(jws, 'sign').returns('abc123'); + const getExpirationTime = sandbox + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .stub(jwtaccess.JWTAccess as any, 'getExpirationTime') + .returns(Date.now() / 1000 + 5); // expire in 5 seconds. + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + // The second time we fetch headers should not cause getExpirationTime + // to be invoked a second time: + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledTwice(getExpirationTime); + sandbox.assert.calledTwice(sign); + }); + + it('returns no headers when no scopes or audiences are provided', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual(headers, {}); + }); + }); }); diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 5861dd3d..08410db4 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -82,9 +82,7 @@ describe('transporters', () => { url: '', }; let configuredOpts = transporter.configure(opts); - console.info(configuredOpts); configuredOpts = transporter.configure(opts); - console.info(configuredOpts); assert( /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test( configuredOpts.headers!['x-goog-api-client'] From 73f0b0f99a67ec099bc5e6f74f9396dcbadb7c32 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2020 11:42:21 -0700 Subject: [PATCH 189/662] chore: release 6.1.0 (#1057) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daba9d57..2f40cf64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [6.1.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.8...v6.1.0) (2020-09-22) + + +### Features + +* default self-signed JWTs ([#1054](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1054)) ([b4d139d](https://www.github.com/googleapis/google-auth-library-nodejs/commit/b4d139d9ee27f886ca8cc5478615c052700fff48)) + ### [6.0.8](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.7...v6.0.8) (2020-08-13) diff --git a/package.json b/package.json index f5f04166..276ffab1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.0.8", + "version": "6.1.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 57e6e542..e4fe63fc 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.8", + "google-auth-library": "^6.1.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 2004c42087bedfd2ff4b864e77065bfc3496ceca Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 1 Oct 2020 04:39:47 -0700 Subject: [PATCH 190/662] chore: update bucket for cloud-rad (#1061) Co-authored-by: gcf-merge-on-green[bot] <60162190+gcf-merge-on-green[bot]@users.noreply.github.com> Source-Author: F. Hinkelmann Source-Date: Wed Sep 30 14:13:57 2020 -0400 Source-Repo: googleapis/synthtool Source-Sha: 079dcce498117f9570cebe6e6cff254b38ba3860 Source-Link: https://github.com/googleapis/synthtool/commit/079dcce498117f9570cebe6e6cff254b38ba3860 --- .kokoro/release/docs-devsite.sh | 2 +- synth.metadata | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index fa089cf2..458fe4f9 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -61,7 +61,7 @@ if [[ -z "$CREDENTIALS" ]]; then CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account fi if [[ -z "$BUCKET" ]]; then - BUCKET=docs-staging-v2-staging + BUCKET=docs-staging-v2 fi python3 -m docuploader upload ./_devsite --destination-prefix docfx --credentials $CREDENTIALS --staging-bucket $BUCKET diff --git a/synth.metadata b/synth.metadata index 1da5b692..a8c66fad 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "55b98171efffc2e7dfecd46ff3485382c3ab62f5" + "sha": "73f0b0f99a67ec099bc5e6f74f9396dcbadb7c32" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "fdd03c161003ab97657cc0218f25c82c89ddf4b6" + "sha": "079dcce498117f9570cebe6e6cff254b38ba3860" } } ], From fb9f88d026354b05c998926398fd5753d7796a7f Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 5 Oct 2020 10:42:28 -0700 Subject: [PATCH 191/662] build(node_library): migrate to Trampoline V2 (#1062) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/92424049-be37-4899-bb0c-b2b51f64b7af/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/0c868d49b8e05bc1f299bc773df9eb4ef9ed96e9 --- .kokoro/common.cfg | 2 +- .kokoro/continuous/node10/common.cfg | 2 +- .kokoro/continuous/node12/common.cfg | 2 +- .kokoro/docs.sh | 2 +- .kokoro/lint.sh | 2 +- .kokoro/populate-secrets.sh | 65 +++- .kokoro/presubmit/node10/common.cfg | 2 +- .kokoro/presubmit/node12/common.cfg | 2 +- .kokoro/publish.sh | 2 +- .kokoro/release/docs-devsite.cfg | 2 +- .kokoro/release/docs-devsite.sh | 4 +- .kokoro/release/docs.cfg | 2 +- .kokoro/release/docs.sh | 4 +- .kokoro/release/publish.cfg | 6 +- .kokoro/samples-test.sh | 2 +- .kokoro/system-test.sh | 2 +- .kokoro/test.sh | 2 +- .kokoro/trampoline.sh | 4 + .kokoro/trampoline_v2.sh | 488 +++++++++++++++++++++++++++ .trampolinerc | 51 +++ synth.metadata | 6 +- 21 files changed, 616 insertions(+), 38 deletions(-) create mode 100755 .kokoro/trampoline_v2.sh create mode 100644 .trampolinerc diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index e3c7faf0..03e6b50b 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -11,7 +11,7 @@ action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { diff --git a/.kokoro/continuous/node10/common.cfg b/.kokoro/continuous/node10/common.cfg index 0792fe1c..d4231f90 100644 --- a/.kokoro/continuous/node10/common.cfg +++ b/.kokoro/continuous/node10/common.cfg @@ -21,7 +21,7 @@ before_action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { diff --git a/.kokoro/continuous/node12/common.cfg b/.kokoro/continuous/node12/common.cfg index f2825615..8e9e508e 100644 --- a/.kokoro/continuous/node12/common.cfg +++ b/.kokoro/continuous/node12/common.cfg @@ -11,7 +11,7 @@ action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { diff --git a/.kokoro/docs.sh b/.kokoro/docs.sh index 952403fa..85901242 100755 --- a/.kokoro/docs.sh +++ b/.kokoro/docs.sh @@ -16,7 +16,7 @@ set -eo pipefail -export NPM_CONFIG_PREFIX=/home/node/.npm-global +export NPM_CONFIG_PREFIX=${HOME}/.npm-global cd $(dirname $0)/.. diff --git a/.kokoro/lint.sh b/.kokoro/lint.sh index b03cb043..aef4866e 100755 --- a/.kokoro/lint.sh +++ b/.kokoro/lint.sh @@ -16,7 +16,7 @@ set -eo pipefail -export NPM_CONFIG_PREFIX=/home/node/.npm-global +export NPM_CONFIG_PREFIX=${HOME}/.npm-global cd $(dirname $0)/.. diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh index 6f9d2288..deb2b199 100755 --- a/.kokoro/populate-secrets.sh +++ b/.kokoro/populate-secrets.sh @@ -13,31 +13,64 @@ # See the License for the specific language governing permissions and # limitations under the License. +# This file is called in the early stage of `trampoline_v2.sh` to +# populate secrets needed for the CI builds. + set -eo pipefail function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;} function msg { println "$*" >&2 ;} function println { printf '%s\n' "$(now) $*" ;} +# Populates requested secrets set in SECRET_MANAGER_KEYS + +# In Kokoro CI builds, we use the service account attached to the +# Kokoro VM. This means we need to setup auth on other CI systems. +# For local run, we just use the gcloud command for retrieving the +# secrets. + +if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + GCLOUD_COMMANDS=( + "docker" + "run" + "--entrypoint=gcloud" + "--volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR}" + "gcr.io/google.com/cloudsdktool/cloud-sdk" + ) + if [[ "${TRAMPOLINE_CI:-}" == "kokoro" ]]; then + SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" + else + echo "Authentication for this CI system is not implemented yet." + exit 2 + # TODO: Determine appropriate SECRET_LOCATION and the GCLOUD_COMMANDS. + fi +else + # For local run, use /dev/shm or temporary directory for + # KOKORO_GFILE_DIR. + if [[ -d "/dev/shm" ]]; then + export KOKORO_GFILE_DIR=/dev/shm + else + export KOKORO_GFILE_DIR=$(mktemp -d -t ci-XXXXXXXX) + fi + SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" + GCLOUD_COMMANDS=("gcloud") +fi -# Populates requested secrets set in SECRET_MANAGER_KEYS from service account: -# kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com -SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" msg "Creating folder on disk for secrets: ${SECRET_LOCATION}" mkdir -p ${SECRET_LOCATION} + for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") do - msg "Retrieving secret ${key}" - docker run --entrypoint=gcloud \ - --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \ - gcr.io/google.com/cloudsdktool/cloud-sdk \ - secrets versions access latest \ - --project cloud-devrel-kokoro-resources \ - --secret $key > \ - "$SECRET_LOCATION/$key" - if [[ $? == 0 ]]; then - msg "Secret written to ${SECRET_LOCATION}/${key}" - else - msg "Error retrieving secret ${key}" - fi + msg "Retrieving secret ${key}" + "${GCLOUD_COMMANDS[@]}" \ + secrets versions access latest \ + --project cloud-devrel-kokoro-resources \ + --secret $key > \ + "$SECRET_LOCATION/$key" + if [[ $? == 0 ]]; then + msg "Secret written to ${SECRET_LOCATION}/${key}" + else + msg "Error retrieving secret ${key}" + exit 2 + fi done diff --git a/.kokoro/presubmit/node10/common.cfg b/.kokoro/presubmit/node10/common.cfg index 0792fe1c..d4231f90 100644 --- a/.kokoro/presubmit/node10/common.cfg +++ b/.kokoro/presubmit/node10/common.cfg @@ -21,7 +21,7 @@ before_action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { diff --git a/.kokoro/presubmit/node12/common.cfg b/.kokoro/presubmit/node12/common.cfg index f2825615..8e9e508e 100644 --- a/.kokoro/presubmit/node12/common.cfg +++ b/.kokoro/presubmit/node12/common.cfg @@ -11,7 +11,7 @@ action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { diff --git a/.kokoro/publish.sh b/.kokoro/publish.sh index f056d861..4db6bf1c 100755 --- a/.kokoro/publish.sh +++ b/.kokoro/publish.sh @@ -16,7 +16,7 @@ set -eo pipefail -export NPM_CONFIG_PREFIX=/home/node/.npm-global +export NPM_CONFIG_PREFIX=${HOME}/.npm-global # Start the releasetool reporter python3 -m pip install gcp-releasetool diff --git a/.kokoro/release/docs-devsite.cfg b/.kokoro/release/docs-devsite.cfg index 906330c2..c37e25cd 100644 --- a/.kokoro/release/docs-devsite.cfg +++ b/.kokoro/release/docs-devsite.cfg @@ -18,7 +18,7 @@ env_vars: { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index 458fe4f9..0d11b7ae 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -20,8 +20,8 @@ set -eo pipefail if [[ -z "$CREDENTIALS" ]]; then # if CREDENTIALS are explicitly set, assume we're testing locally # and don't set NPM_CONFIG_PREFIX. - export NPM_CONFIG_PREFIX=/home/node/.npm-global - export PATH="$PATH:/home/node/.npm-global/bin" + export NPM_CONFIG_PREFIX=${HOME}/.npm-global + export PATH="$PATH:${NPM_CONFIG_PREFIX}/bin" cd $(dirname $0)/../.. fi diff --git a/.kokoro/release/docs.cfg b/.kokoro/release/docs.cfg index 344676ea..8f8a1316 100644 --- a/.kokoro/release/docs.cfg +++ b/.kokoro/release/docs.cfg @@ -18,7 +18,7 @@ env_vars: { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/release/docs.sh b/.kokoro/release/docs.sh index 4d3a0868..4c866c86 100755 --- a/.kokoro/release/docs.sh +++ b/.kokoro/release/docs.sh @@ -20,8 +20,8 @@ set -eo pipefail if [[ -z "$CREDENTIALS" ]]; then # if CREDENTIALS are explicitly set, assume we're testing locally # and don't set NPM_CONFIG_PREFIX. - export NPM_CONFIG_PREFIX=/home/node/.npm-global - export PATH="$PATH:/home/node/.npm-global/bin" + export NPM_CONFIG_PREFIX=${HOME}/.npm-global + export PATH="$PATH:${NPM_CONFIG_PREFIX}/bin" cd $(dirname $0)/../.. fi npm install diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index a70341d4..15dee6a1 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -27,7 +27,7 @@ before_action { } } -# Fetch magictoken to use with Magic Github Proxy +# Fetch magictoken to use with Magic Github Proxy before_action { fetch_keystore { keystore_resource { @@ -37,7 +37,7 @@ before_action { } } -# Fetch api key to use with Magic Github Proxy +# Fetch api key to use with Magic Github Proxy before_action { fetch_keystore { keystore_resource { @@ -56,7 +56,7 @@ env_vars: { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index c0c40139..bab7ba4e 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -16,7 +16,7 @@ set -eo pipefail -export NPM_CONFIG_PREFIX=/home/node/.npm-global +export NPM_CONFIG_PREFIX=${HOME}/.npm-global # Setup service account credentials. export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 283f1700..8a084004 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -16,7 +16,7 @@ set -eo pipefail -export NPM_CONFIG_PREFIX=/home/node/.npm-global +export NPM_CONFIG_PREFIX=${HOME}/.npm-global # Setup service account credentials. export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json diff --git a/.kokoro/test.sh b/.kokoro/test.sh index 47be59b9..5be385fe 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -16,7 +16,7 @@ set -eo pipefail -export NPM_CONFIG_PREFIX=/home/node/.npm-global +export NPM_CONFIG_PREFIX=${HOME}/.npm-global cd $(dirname $0)/.. diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh index a4241db2..f693a1ce 100755 --- a/.kokoro/trampoline.sh +++ b/.kokoro/trampoline.sh @@ -13,6 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +# This file is not used any more, but we keep this file for making it +# easy to roll back. +# TODO: Remove this file from the template. + set -eo pipefail # Always run the cleanup script, regardless of the success of bouncing into diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh new file mode 100755 index 00000000..5ae75f97 --- /dev/null +++ b/.kokoro/trampoline_v2.sh @@ -0,0 +1,488 @@ +#!/usr/bin/env bash +# Copyright 2020 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. + +# trampoline_v2.sh +# +# If you want to make a change to this file, consider doing so at: +# https://github.com/googlecloudplatform/docker-ci-helper +# +# This script is for running CI builds. For Kokoro builds, we +# set this script to `build_file` field in the Kokoro configuration. + +# This script does 3 things. +# +# 1. Prepare the Docker image for the test +# 2. Run the Docker with appropriate flags to run the test +# 3. Upload the newly built Docker image +# +# in a way that is somewhat compatible with trampoline_v1. +# +# These environment variables are required: +# TRAMPOLINE_IMAGE: The docker image to use. +# TRAMPOLINE_DOCKERFILE: The location of the Dockerfile. +# +# You can optionally change these environment variables: +# TRAMPOLINE_IMAGE_UPLOAD: +# (true|false): Whether to upload the Docker image after the +# successful builds. +# TRAMPOLINE_BUILD_FILE: The script to run in the docker container. +# TRAMPOLINE_WORKSPACE: The workspace path in the docker container. +# Defaults to /workspace. +# Potentially there are some repo specific envvars in .trampolinerc in +# the project root. +# +# Here is an example for running this script. +# TRAMPOLINE_IMAGE=gcr.io/cloud-devrel-kokoro-resources/node:10-user \ +# TRAMPOLINE_BUILD_FILE=.kokoro/system-test.sh \ +# .kokoro/trampoline_v2.sh + +set -euo pipefail + +TRAMPOLINE_VERSION="2.0.7" + +if command -v tput >/dev/null && [[ -n "${TERM:-}" ]]; then + readonly IO_COLOR_RED="$(tput setaf 1)" + readonly IO_COLOR_GREEN="$(tput setaf 2)" + readonly IO_COLOR_YELLOW="$(tput setaf 3)" + readonly IO_COLOR_RESET="$(tput sgr0)" +else + readonly IO_COLOR_RED="" + readonly IO_COLOR_GREEN="" + readonly IO_COLOR_YELLOW="" + readonly IO_COLOR_RESET="" +fi + +function function_exists { + [ $(LC_ALL=C type -t $1)"" == "function" ] +} + +# Logs a message using the given color. The first argument must be one +# of the IO_COLOR_* variables defined above, such as +# "${IO_COLOR_YELLOW}". The remaining arguments will be logged in the +# given color. The log message will also have an RFC-3339 timestamp +# prepended (in UTC). You can disable the color output by setting +# TERM=vt100. +function log_impl() { + local color="$1" + shift + local timestamp="$(date -u "+%Y-%m-%dT%H:%M:%SZ")" + echo "================================================================" + echo "${color}${timestamp}:" "$@" "${IO_COLOR_RESET}" + echo "================================================================" +} + +# Logs the given message with normal coloring and a timestamp. +function log() { + log_impl "${IO_COLOR_RESET}" "$@" +} + +# Logs the given message in green with a timestamp. +function log_green() { + log_impl "${IO_COLOR_GREEN}" "$@" +} + +# Logs the given message in yellow with a timestamp. +function log_yellow() { + log_impl "${IO_COLOR_YELLOW}" "$@" +} + +# Logs the given message in red with a timestamp. +function log_red() { + log_impl "${IO_COLOR_RED}" "$@" +} + +readonly tmpdir=$(mktemp -d -t ci-XXXXXXXX) +readonly tmphome="${tmpdir}/h" +mkdir -p "${tmphome}" + +function cleanup() { + rm -rf "${tmpdir}" +} +trap cleanup EXIT + +RUNNING_IN_CI="${RUNNING_IN_CI:-false}" + +# The workspace in the container, defaults to /workspace. +TRAMPOLINE_WORKSPACE="${TRAMPOLINE_WORKSPACE:-/workspace}" + +pass_down_envvars=( + # TRAMPOLINE_V2 variables. + # Tells scripts whether they are running as part of CI or not. + "RUNNING_IN_CI" + # Indicates which CI system we're in. + "TRAMPOLINE_CI" + # Indicates the version of the script. + "TRAMPOLINE_VERSION" +) + +log_yellow "Building with Trampoline ${TRAMPOLINE_VERSION}" + +# Detect which CI systems we're in. If we're in any of the CI systems +# we support, `RUNNING_IN_CI` will be true and `TRAMPOLINE_CI` will be +# the name of the CI system. Both envvars will be passing down to the +# container for telling which CI system we're in. +if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then + # descriptive env var for indicating it's on CI. + RUNNING_IN_CI="true" + TRAMPOLINE_CI="kokoro" + if [[ "${TRAMPOLINE_USE_LEGACY_SERVICE_ACCOUNT:-}" == "true" ]]; then + if [[ ! -f "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" ]]; then + log_red "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json does not exist. Did you forget to mount cloud-devrel-kokoro-resources/trampoline? Aborting." + exit 1 + fi + # This service account will be activated later. + TRAMPOLINE_SERVICE_ACCOUNT="${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" + else + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + gcloud auth list + fi + log_yellow "Configuring Container Registry access" + gcloud auth configure-docker --quiet + fi + pass_down_envvars+=( + # KOKORO dynamic variables. + "KOKORO_BUILD_NUMBER" + "KOKORO_BUILD_ID" + "KOKORO_JOB_NAME" + "KOKORO_GIT_COMMIT" + "KOKORO_GITHUB_COMMIT" + "KOKORO_GITHUB_PULL_REQUEST_NUMBER" + "KOKORO_GITHUB_PULL_REQUEST_COMMIT" + # For Build Cop Bot + "KOKORO_GITHUB_COMMIT_URL" + "KOKORO_GITHUB_PULL_REQUEST_URL" + ) +elif [[ "${TRAVIS:-}" == "true" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="travis" + pass_down_envvars+=( + "TRAVIS_BRANCH" + "TRAVIS_BUILD_ID" + "TRAVIS_BUILD_NUMBER" + "TRAVIS_BUILD_WEB_URL" + "TRAVIS_COMMIT" + "TRAVIS_COMMIT_MESSAGE" + "TRAVIS_COMMIT_RANGE" + "TRAVIS_JOB_NAME" + "TRAVIS_JOB_NUMBER" + "TRAVIS_JOB_WEB_URL" + "TRAVIS_PULL_REQUEST" + "TRAVIS_PULL_REQUEST_BRANCH" + "TRAVIS_PULL_REQUEST_SHA" + "TRAVIS_PULL_REQUEST_SLUG" + "TRAVIS_REPO_SLUG" + "TRAVIS_SECURE_ENV_VARS" + "TRAVIS_TAG" + ) +elif [[ -n "${GITHUB_RUN_ID:-}" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="github-workflow" + pass_down_envvars+=( + "GITHUB_WORKFLOW" + "GITHUB_RUN_ID" + "GITHUB_RUN_NUMBER" + "GITHUB_ACTION" + "GITHUB_ACTIONS" + "GITHUB_ACTOR" + "GITHUB_REPOSITORY" + "GITHUB_EVENT_NAME" + "GITHUB_EVENT_PATH" + "GITHUB_SHA" + "GITHUB_REF" + "GITHUB_HEAD_REF" + "GITHUB_BASE_REF" + ) +elif [[ "${CIRCLECI:-}" == "true" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="circleci" + pass_down_envvars+=( + "CIRCLE_BRANCH" + "CIRCLE_BUILD_NUM" + "CIRCLE_BUILD_URL" + "CIRCLE_COMPARE_URL" + "CIRCLE_JOB" + "CIRCLE_NODE_INDEX" + "CIRCLE_NODE_TOTAL" + "CIRCLE_PREVIOUS_BUILD_NUM" + "CIRCLE_PROJECT_REPONAME" + "CIRCLE_PROJECT_USERNAME" + "CIRCLE_REPOSITORY_URL" + "CIRCLE_SHA1" + "CIRCLE_STAGE" + "CIRCLE_USERNAME" + "CIRCLE_WORKFLOW_ID" + "CIRCLE_WORKFLOW_JOB_ID" + "CIRCLE_WORKFLOW_UPSTREAM_JOB_IDS" + "CIRCLE_WORKFLOW_WORKSPACE_ID" + ) +fi + +# Configure the service account for pulling the docker image. +function repo_root() { + local dir="$1" + while [[ ! -d "${dir}/.git" ]]; do + dir="$(dirname "$dir")" + done + echo "${dir}" +} + +# Detect the project root. In CI builds, we assume the script is in +# the git tree and traverse from there, otherwise, traverse from `pwd` +# to find `.git` directory. +if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + PROGRAM_PATH="$(realpath "$0")" + PROGRAM_DIR="$(dirname "${PROGRAM_PATH}")" + PROJECT_ROOT="$(repo_root "${PROGRAM_DIR}")" +else + PROJECT_ROOT="$(repo_root $(pwd))" +fi + +log_yellow "Changing to the project root: ${PROJECT_ROOT}." +cd "${PROJECT_ROOT}" + +# To support relative path for `TRAMPOLINE_SERVICE_ACCOUNT`, we need +# to use this environment variable in `PROJECT_ROOT`. +if [[ -n "${TRAMPOLINE_SERVICE_ACCOUNT:-}" ]]; then + + mkdir -p "${tmpdir}/gcloud" + gcloud_config_dir="${tmpdir}/gcloud" + + log_yellow "Using isolated gcloud config: ${gcloud_config_dir}." + export CLOUDSDK_CONFIG="${gcloud_config_dir}" + + log_yellow "Using ${TRAMPOLINE_SERVICE_ACCOUNT} for authentication." + gcloud auth activate-service-account \ + --key-file "${TRAMPOLINE_SERVICE_ACCOUNT}" + log_yellow "Configuring Container Registry access" + gcloud auth configure-docker --quiet +fi + +required_envvars=( + # The basic trampoline configurations. + "TRAMPOLINE_IMAGE" + "TRAMPOLINE_BUILD_FILE" +) + +if [[ -f "${PROJECT_ROOT}/.trampolinerc" ]]; then + source "${PROJECT_ROOT}/.trampolinerc" +fi + +log_yellow "Checking environment variables." +for e in "${required_envvars[@]}" +do + if [[ -z "${!e:-}" ]]; then + log "Missing ${e} env var. Aborting." + exit 1 + fi +done + +# We want to support legacy style TRAMPOLINE_BUILD_FILE used with V1 +# script: e.g. "github/repo-name/.kokoro/run_tests.sh" +TRAMPOLINE_BUILD_FILE="${TRAMPOLINE_BUILD_FILE#github/*/}" +log_yellow "Using TRAMPOLINE_BUILD_FILE: ${TRAMPOLINE_BUILD_FILE}" + +# ignore error on docker operations and test execution +set +e + +log_yellow "Preparing Docker image." +# We only download the docker image in CI builds. +if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + # Download the docker image specified by `TRAMPOLINE_IMAGE` + + # We may want to add --max-concurrent-downloads flag. + + log_yellow "Start pulling the Docker image: ${TRAMPOLINE_IMAGE}." + if docker pull "${TRAMPOLINE_IMAGE}"; then + log_green "Finished pulling the Docker image: ${TRAMPOLINE_IMAGE}." + has_image="true" + else + log_red "Failed pulling the Docker image: ${TRAMPOLINE_IMAGE}." + has_image="false" + fi +else + # For local run, check if we have the image. + if docker images "${TRAMPOLINE_IMAGE}" | grep "${TRAMPOLINE_IMAGE%:*}"; then + has_image="true" + else + has_image="false" + fi +fi + + +# The default user for a Docker container has uid 0 (root). To avoid +# creating root-owned files in the build directory we tell docker to +# use the current user ID. +user_uid="$(id -u)" +user_gid="$(id -g)" +user_name="$(id -un)" + +# To allow docker in docker, we add the user to the docker group in +# the host os. +docker_gid=$(cut -d: -f3 < <(getent group docker)) + +update_cache="false" +if [[ "${TRAMPOLINE_DOCKERFILE:-none}" != "none" ]]; then + # Build the Docker image from the source. + context_dir=$(dirname "${TRAMPOLINE_DOCKERFILE}") + docker_build_flags=( + "-f" "${TRAMPOLINE_DOCKERFILE}" + "-t" "${TRAMPOLINE_IMAGE}" + "--build-arg" "UID=${user_uid}" + "--build-arg" "USERNAME=${user_name}" + ) + if [[ "${has_image}" == "true" ]]; then + docker_build_flags+=("--cache-from" "${TRAMPOLINE_IMAGE}") + fi + + log_yellow "Start building the docker image." + if [[ "${TRAMPOLINE_VERBOSE:-false}" == "true" ]]; then + echo "docker build" "${docker_build_flags[@]}" "${context_dir}" + fi + + # ON CI systems, we want to suppress docker build logs, only + # output the logs when it fails. + if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + if docker build "${docker_build_flags[@]}" "${context_dir}" \ + > "${tmpdir}/docker_build.log" 2>&1; then + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + cat "${tmpdir}/docker_build.log" + fi + + log_green "Finished building the docker image." + update_cache="true" + else + log_red "Failed to build the Docker image, aborting." + log_yellow "Dumping the build logs:" + cat "${tmpdir}/docker_build.log" + exit 1 + fi + else + if docker build "${docker_build_flags[@]}" "${context_dir}"; then + log_green "Finished building the docker image." + update_cache="true" + else + log_red "Failed to build the Docker image, aborting." + exit 1 + fi + fi +else + if [[ "${has_image}" != "true" ]]; then + log_red "We do not have ${TRAMPOLINE_IMAGE} locally, aborting." + exit 1 + fi +fi + +# We use an array for the flags so they are easier to document. +docker_flags=( + # Remove the container after it exists. + "--rm" + + # Use the host network. + "--network=host" + + # Run in priviledged mode. We are not using docker for sandboxing or + # isolation, just for packaging our dev tools. + "--privileged" + + # Run the docker script with the user id. Because the docker image gets to + # write in ${PWD} you typically want this to be your user id. + # To allow docker in docker, we need to use docker gid on the host. + "--user" "${user_uid}:${docker_gid}" + + # Pass down the USER. + "--env" "USER=${user_name}" + + # Mount the project directory inside the Docker container. + "--volume" "${PROJECT_ROOT}:${TRAMPOLINE_WORKSPACE}" + "--workdir" "${TRAMPOLINE_WORKSPACE}" + "--env" "PROJECT_ROOT=${TRAMPOLINE_WORKSPACE}" + + # Mount the temporary home directory. + "--volume" "${tmphome}:/h" + "--env" "HOME=/h" + + # Allow docker in docker. + "--volume" "/var/run/docker.sock:/var/run/docker.sock" + + # Mount the /tmp so that docker in docker can mount the files + # there correctly. + "--volume" "/tmp:/tmp" + # Pass down the KOKORO_GFILE_DIR and KOKORO_KEYSTORE_DIR + # TODO(tmatsuo): This part is not portable. + "--env" "TRAMPOLINE_SECRET_DIR=/secrets" + "--volume" "${KOKORO_GFILE_DIR:-/dev/shm}:/secrets/gfile" + "--env" "KOKORO_GFILE_DIR=/secrets/gfile" + "--volume" "${KOKORO_KEYSTORE_DIR:-/dev/shm}:/secrets/keystore" + "--env" "KOKORO_KEYSTORE_DIR=/secrets/keystore" +) + +# Add an option for nicer output if the build gets a tty. +if [[ -t 0 ]]; then + docker_flags+=("-it") +fi + +# Passing down env vars +for e in "${pass_down_envvars[@]}" +do + if [[ -n "${!e:-}" ]]; then + docker_flags+=("--env" "${e}=${!e}") + fi +done + +# If arguments are given, all arguments will become the commands run +# in the container, otherwise run TRAMPOLINE_BUILD_FILE. +if [[ $# -ge 1 ]]; then + log_yellow "Running the given commands '" "${@:1}" "' in the container." + readonly commands=("${@:1}") + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}" + fi + docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}" +else + log_yellow "Running the tests in a Docker container." + docker_flags+=("--entrypoint=${TRAMPOLINE_BUILD_FILE}") + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" + fi + docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" +fi + + +test_retval=$? + +if [[ ${test_retval} -eq 0 ]]; then + log_green "Build finished with ${test_retval}" +else + log_red "Build finished with ${test_retval}" +fi + +# Only upload it when the test passes. +if [[ "${update_cache}" == "true" ]] && \ + [[ $test_retval == 0 ]] && \ + [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]]; then + log_yellow "Uploading the Docker image." + if docker push "${TRAMPOLINE_IMAGE}"; then + log_green "Finished uploading the Docker image." + else + log_red "Failed uploading the Docker image." + fi + # Call trampoline_after_upload_hook if it's defined. + if function_exists trampoline_after_upload_hook; then + trampoline_after_upload_hook + fi + +fi + +exit "${test_retval}" diff --git a/.trampolinerc b/.trampolinerc new file mode 100644 index 00000000..164613b9 --- /dev/null +++ b/.trampolinerc @@ -0,0 +1,51 @@ +# Copyright 2020 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. + +# Template for .trampolinerc + +# Add required env vars here. +required_envvars+=( +) + +# Add env vars which are passed down into the container here. +pass_down_envvars+=( + "AUTORELEASE_PR" +) + +# Prevent unintentional override on the default image. +if [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]] && \ + [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then + echo "Please set TRAMPOLINE_IMAGE if you want to upload the Docker image." + exit 1 +fi + +# Define the default value if it makes sense. +if [[ -z "${TRAMPOLINE_IMAGE_UPLOAD:-}" ]]; then + TRAMPOLINE_IMAGE_UPLOAD="" +fi + +if [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then + TRAMPOLINE_IMAGE="" +fi + +if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then + TRAMPOLINE_DOCKERFILE="" +fi + +if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then + TRAMPOLINE_BUILD_FILE="" +fi + +# Secret Manager secrets. +source ${PROJECT_ROOT}/.kokoro/populate-secrets.sh diff --git a/synth.metadata b/synth.metadata index a8c66fad..0e0ec2c4 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "73f0b0f99a67ec099bc5e6f74f9396dcbadb7c32" + "sha": "2004c42087bedfd2ff4b864e77065bfc3496ceca" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "079dcce498117f9570cebe6e6cff254b38ba3860" + "sha": "0c868d49b8e05bc1f299bc773df9eb4ef9ed96e9" } } ], @@ -54,10 +54,12 @@ ".kokoro/test.bat", ".kokoro/test.sh", ".kokoro/trampoline.sh", + ".kokoro/trampoline_v2.sh", ".mocharc.js", ".nycrc", ".prettierignore", ".prettierrc.js", + ".trampolinerc", "CODE_OF_CONDUCT.md", "CONTRIBUTING.md", "LICENSE", From 9116f247486d6376feca505bbfa42a91d5e579e2 Mon Sep 17 00:00:00 2001 From: Stephen Date: Tue, 6 Oct 2020 12:38:11 -0400 Subject: [PATCH 192/662] fix(deps): upgrade gtoken (#1064) Addresses a potential prototype pollution leak: https://github.com/googleapis/node-gtoken/pull/337, https://github.com/digitalbazaar/forge/blob/588c41062d9a13f8dc91be3723b159c6cc434b15/CHANGELOG.md --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 276ffab1..8e0a4100 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "fast-text-encoding": "^1.0.0", "gaxios": "^3.0.0", "gcp-metadata": "^4.1.0", - "gtoken": "^5.0.0", + "gtoken": "^5.0.4", "jws": "^4.0.0", "lru-cache": "^6.0.0" }, From 084da82a00d0fe3352b51f72a3c64468dfcf1eef Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2020 12:44:22 -0400 Subject: [PATCH 193/662] chore: release 6.1.1 (#1065) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f40cf64..e1683cf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.1.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.0...v6.1.1) (2020-10-06) + + +### Bug Fixes + +* **deps:** upgrade gtoken ([#1064](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1064)) ([9116f24](https://www.github.com/googleapis/google-auth-library-nodejs/commit/9116f247486d6376feca505bbfa42a91d5e579e2)) + ## [6.1.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.0.8...v6.1.0) (2020-09-22) diff --git a/package.json b/package.json index 8e0a4100..73a5ca01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.1.0", + "version": "6.1.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index e4fe63fc..f0af02e2 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.1.0", + "google-auth-library": "^6.1.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 2f4ca85240e18227612f98fbb87f9c7eef16700b Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Thu, 15 Oct 2020 19:29:02 -0700 Subject: [PATCH 194/662] build: only check engines on prod (#1077) --- .github/workflows/ci.yaml | 6 +++++- synth.metadata | 16 ++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7dd110e3..06067a8c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,11 @@ jobs: with: node-version: ${{ matrix.node }} - run: node --version - - run: npm install --engine-strict + # The first installation step ensures that all of our production + # dependencies work on the given Node.js version, this helps us find + # dependencies that don't match our engines field: + - run: npm install --production --engine-strict + - run: npm install - run: npm test - name: coverage uses: codecov/codecov-action@v1 diff --git a/synth.metadata b/synth.metadata index 0e0ec2c4..a630911d 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,15 +3,8 @@ { "git": { "name": ".", - "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "2004c42087bedfd2ff4b864e77065bfc3496ceca" - } - }, - { - "git": { - "name": "synthtool", - "remote": "https://github.com/googleapis/synthtool.git", - "sha": "0c868d49b8e05bc1f299bc773df9eb4ef9ed96e9" + "remote": "git@github.com:googleapis/google-auth-library-nodejs.git", + "sha": "084da82a00d0fe3352b51f72a3c64468dfcf1eef" } } ], @@ -64,7 +57,6 @@ "CONTRIBUTING.md", "LICENSE", "api-extractor.json", - "renovate.json", - "samples/README.md" + "renovate.json" ] -} \ No newline at end of file +} From a1893b11658d18dbf1806e62292c5986074543f3 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 16 Oct 2020 05:10:07 +0200 Subject: [PATCH 195/662] chore(deps): update dependency webpack-cli to v4 (#1075) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [webpack-cli](https://togithub.com/webpack/webpack-cli) | devDependencies | major | [`^3.1.1` -> `^4.0.0`](https://renovatebot.com/diffs/npm/webpack-cli/3.3.12/4.0.0) | --- ### Release Notes
webpack/webpack-cli ### [`v4.0.0`](https://togithub.com/webpack/webpack-cli/blob/master/CHANGELOG.md#​400-httpsgithubcomwebpackwebpack-clicomparewebpack-cli400-rc1webpack-cli400-2020-10-10) [Compare Source](https://togithub.com/webpack/webpack-cli/compare/v3.3.12...webpack-cli@4.0.0) ##### Bug Fixes - add compilation lifecycle in watch instance ([#​1903](https://togithub.com/webpack/webpack-cli/issues/1903)) ([02b6d21](https://togithub.com/webpack/webpack-cli/commit/02b6d21eaa20166a7ed37816de716b8fc22b756a)) - cleanup `package-utils` package ([#​1822](https://togithub.com/webpack/webpack-cli/issues/1822)) ([fd5b92b](https://togithub.com/webpack/webpack-cli/commit/fd5b92b3cd40361daec5bf4486e455a41f4c9738)) - cli-executer supplies args further up ([#​1904](https://togithub.com/webpack/webpack-cli/issues/1904)) ([097564a](https://togithub.com/webpack/webpack-cli/commit/097564a851b36b63e0a6bf88144997ef65aa057a)) - exit code for validation errors ([59f6303](https://togithub.com/webpack/webpack-cli/commit/59f63037fcbdbb8934b578b9adf5725bc4ae1235)) - exit process in case of schema errors ([71e89b4](https://togithub.com/webpack/webpack-cli/commit/71e89b4092d953ea587cc4f606451ab78cbcdb93)) ##### Features - assign config paths in build dependencies in cache config ([#​1900](https://togithub.com/webpack/webpack-cli/issues/1900)) ([7e90f11](https://togithub.com/webpack/webpack-cli/commit/7e90f110b119f36ef9def4f66cf4e17ccf1438cd))
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- system-test/fixtures/kitchen/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 73a5ca01..b73d3ed4 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "ts-loader": "^8.0.0", "typescript": "^3.8.3", "webpack": "^4.20.2", - "webpack-cli": "^3.1.1", + "webpack-cli": "^4.0.0", "@microsoft/api-documenter": "^7.8.10", "@microsoft/api-extractor": "^7.8.10" }, diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index 076408f4..49f2f1fd 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -23,6 +23,6 @@ "null-loader": "^4.0.0", "ts-loader": "^8.0.0", "webpack": "^4.20.2", - "webpack-cli": "^3.1.1" + "webpack-cli": "^4.0.0" } } From 125fe0924a2206ebb0c83ece9947524e7b135803 Mon Sep 17 00:00:00 2001 From: Megan Potter <57276408+feywind@users.noreply.github.com> Date: Mon, 19 Oct 2020 09:57:59 -0700 Subject: [PATCH 196/662] fix: update gcp-metadata to catch a json-bigint security fix (#1078) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b73d3ed4..00bc2931 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^3.0.0", - "gcp-metadata": "^4.1.0", + "gcp-metadata": "^4.2.0", "gtoken": "^5.0.4", "jws": "^4.0.0", "lru-cache": "^6.0.0" From 70aa40ed18c5b21e4d54c671eaf2488da62e16a0 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2020 13:16:21 -0400 Subject: [PATCH 197/662] chore: release 6.1.2 (#1080) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1683cf7..5602df7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.1.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.1...v6.1.2) (2020-10-19) + + +### Bug Fixes + +* update gcp-metadata to catch a json-bigint security fix ([#1078](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1078)) ([125fe09](https://www.github.com/googleapis/google-auth-library-nodejs/commit/125fe0924a2206ebb0c83ece9947524e7b135803)) + ### [6.1.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.0...v6.1.1) (2020-10-06) diff --git a/package.json b/package.json index 00bc2931..31cf40c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.1.1", + "version": "6.1.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index f0af02e2..f6d7e942 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.1.1", + "google-auth-library": "^6.1.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 2e570c76eaed077d7f7221581e6913294868590f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 21 Oct 2020 15:59:02 +0200 Subject: [PATCH 198/662] chore(deps): update dependency karma-firefox-launcher to v2 (#1082) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31cf40c8..7e577096 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "karma": "^5.0.0", "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^2.0.0", - "karma-firefox-launcher": "^1.1.0", + "karma-firefox-launcher": "^2.0.0", "karma-mocha": "^2.0.0", "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", From 656366dcb4f1e84d43de07e3ccbd90d521cf4cbd Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 21 Oct 2020 11:17:13 -0700 Subject: [PATCH 199/662] changes without context (#1083) autosynth cannot find the source of changes triggered by earlier changes in this repository, or by version upgrades to tools such as linters. --- .kokoro/release/docs-devsite.sh | 4 ++++ synth.metadata | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index 0d11b7ae..7657be33 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -37,9 +37,13 @@ NAME=$(cat .repo-metadata.json | json name) mkdir ./_devsite cp ./yaml/$NAME/* ./_devsite +# Clean up TOC # Delete SharePoint item, see https://github.com/microsoft/rushstack/issues/1229 sed -i -e '1,3d' ./yaml/toc.yml sed -i -e 's/^ //' ./yaml/toc.yml +# Delete interfaces from TOC (name and uid) +sed -i -e '/name: I[A-Z]/{N;d;}' ./yaml/toc.yml +sed -i -e '/^ *\@google-cloud.*:interface/d' ./yaml/toc.yml cp ./yaml/toc.yml ./_devsite/toc.yml diff --git a/synth.metadata b/synth.metadata index a630911d..5be6f3cf 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,8 +3,15 @@ { "git": { "name": ".", - "remote": "git@github.com:googleapis/google-auth-library-nodejs.git", - "sha": "084da82a00d0fe3352b51f72a3c64468dfcf1eef" + "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", + "sha": "2e570c76eaed077d7f7221581e6913294868590f" + } + }, + { + "git": { + "name": "synthtool", + "remote": "https://github.com/googleapis/synthtool.git", + "sha": "901ddd44e9ef7887ee681b9183bbdea99437fdcc" } } ], @@ -57,6 +64,9 @@ "CONTRIBUTING.md", "LICENSE", "api-extractor.json", - "renovate.json" + "package-lock.json.3525877600", + "renovate.json", + "samples/README.md", + "samples/package-lock.json.2890922137" ] -} +} \ No newline at end of file From f2678ff5f8f5a0ee33924278b58e0a6e3122cb12 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 22 Oct 2020 19:06:15 +0200 Subject: [PATCH 200/662] fix(deps): update dependency gaxios to v4 (#1086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [gaxios](https://togithub.com/googleapis/gaxios) | dependencies | major | [`^3.0.0` -> `^4.0.0`](https://renovatebot.com/diffs/npm/gaxios/3.2.0/4.0.0) | --- ### Release Notes
googleapis/gaxios ### [`v4.0.0`](https://togithub.com/googleapis/gaxios/blob/master/CHANGELOG.md#​400-httpswwwgithubcomgoogleapisgaxioscomparev320v400-2020-10-21) [Compare Source](https://togithub.com/googleapis/gaxios/compare/v3.2.0...v4.0.0) ##### ⚠ BREAKING CHANGES - parameters in `url` and parameters provided via params will now be combined. ##### Bug Fixes - drop requirement on URL/combine url and params ([#​338](https://www.github.com/googleapis/gaxios/issues/338)) ([e166bc6](https://www.github.com/googleapis/gaxios/commit/e166bc6721fd979070ab3d9c69b71ffe9ee061c7))
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e577096..59d77e38 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^3.0.0", + "gaxios": "^4.0.0", "gcp-metadata": "^4.2.0", "gtoken": "^5.0.4", "jws": "^4.0.0", From 51f62e654d0144c7c10eb913db1d8d31189236c3 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 10:29:04 -0700 Subject: [PATCH 201/662] chore: release 6.1.3 (#1087) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5602df7d..4fb98e95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.1.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.2...v6.1.3) (2020-10-22) + + +### Bug Fixes + +* **deps:** update dependency gaxios to v4 ([#1086](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1086)) ([f2678ff](https://www.github.com/googleapis/google-auth-library-nodejs/commit/f2678ff5f8f5a0ee33924278b58e0a6e3122cb12)) + ### [6.1.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.1...v6.1.2) (2020-10-19) diff --git a/package.json b/package.json index 59d77e38..fc810fda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.1.2", + "version": "6.1.3", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index f6d7e942..e4fa34e5 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.1.2", + "google-auth-library": "^6.1.3", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From bf37628284d4e45c7ec5bb0063bc0f4a2fd9e048 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 27 Oct 2020 08:38:03 -0700 Subject: [PATCH 202/662] docs: updated code of conduct (includes update to actions) (#1091) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/d837e2c5-e7f6-4c9f-a98e-12a1b08a381f/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/89c849ba5013e45e8fb688b138f33c2ec6083dc5 Source-Link: https://github.com/googleapis/synthtool/commit/a783321fd55f010709294455584a553f4b24b944 Source-Link: https://github.com/googleapis/synthtool/commit/b7413d38b763827c72c0360f0a3d286c84656eeb Source-Link: https://github.com/googleapis/synthtool/commit/5f6ef0ec5501d33c4667885b37a7685a30d41a76 --- .github/workflows/ci.yaml | 12 ++-- CODE_OF_CONDUCT.md | 123 +++++++++++++++++++++++++++----------- synth.metadata | 8 +-- 3 files changed, 97 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 06067a8c..891c9253 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 13] + node: [10, 12, 14, 15] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 @@ -19,7 +19,9 @@ jobs: # The first installation step ensures that all of our production # dependencies work on the given Node.js version, this helps us find # dependencies that don't match our engines field: - - run: npm install --production --engine-strict + - run: npm install --production --engine-strict --ignore-scripts --no-package-lock + # Clean up the production install, before installing dev/production: + - run: rm -rf node_modules - run: npm install - run: npm test - name: coverage @@ -33,7 +35,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - run: npm install - run: npm test - name: coverage @@ -47,7 +49,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - run: npm install - run: npm run lint docs: @@ -56,6 +58,6 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - run: npm install - run: npm run docs-test diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 46b2a08e..2add2547 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,43 +1,94 @@ -# Contributor Code of Conduct + +# Code of Conduct -As contributors and maintainers of this project, -and in the interest of fostering an open and welcoming community, -we pledge to respect all people who contribute through reporting issues, -posting feature requests, updating documentation, -submitting pull requests or patches, and other activities. +## Our Pledge -We are committed to making participation in this project -a harassment-free experience for everyone, -regardless of level of experience, gender, gender identity and expression, -sexual orientation, disability, personal appearance, -body size, race, ethnicity, age, religion, or nationality. +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, -such as physical or electronic -addresses, without explicit permission -* Other unethical or unprofessional conduct. +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct. -By adopting this Code of Conduct, -project maintainers commit themselves to fairly and consistently -applying these principles to every aspect of managing this project. -Project maintainers who do not follow or enforce the Code of Conduct -may be permanently removed from the project team. - -This code of conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior -may be reported by opening an issue -or contacting one or more of the project maintainers. - -This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, -available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + +Reports should be directed to *googleapis-stewards@google.com*, the +Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html \ No newline at end of file diff --git a/synth.metadata b/synth.metadata index 5be6f3cf..a6d64a3b 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "2e570c76eaed077d7f7221581e6913294868590f" + "sha": "f2678ff5f8f5a0ee33924278b58e0a6e3122cb12" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "901ddd44e9ef7887ee681b9183bbdea99437fdcc" + "sha": "89c849ba5013e45e8fb688b138f33c2ec6083dc5" } } ], @@ -64,9 +64,7 @@ "CONTRIBUTING.md", "LICENSE", "api-extractor.json", - "package-lock.json.3525877600", "renovate.json", - "samples/README.md", - "samples/package-lock.json.2890922137" + "samples/README.md" ] } \ No newline at end of file From 4c3d77c4bd6c1dcfc950e06d199fcb0419aa39d6 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 3 Nov 2020 09:15:20 -0800 Subject: [PATCH 203/662] build(node): add KOKORO_BUILD_ARTIFACTS_SUBDIR to env (#1094) Source-Author: Benjamin E. Coe Source-Date: Mon Nov 2 15:56:09 2020 -0500 Source-Repo: googleapis/synthtool Source-Sha: ba9918cd22874245b55734f57470c719b577e591 Source-Link: https://github.com/googleapis/synthtool/commit/ba9918cd22874245b55734f57470c719b577e591 --- .kokoro/trampoline_v2.sh | 2 ++ synth.metadata | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh index 5ae75f97..606d4321 100755 --- a/.kokoro/trampoline_v2.sh +++ b/.kokoro/trampoline_v2.sh @@ -125,6 +125,8 @@ pass_down_envvars=( "TRAMPOLINE_CI" # Indicates the version of the script. "TRAMPOLINE_VERSION" + # Contains path to build artifacts being executed. + "KOKORO_BUILD_ARTIFACTS_SUBDIR" ) log_yellow "Building with Trampoline ${TRAMPOLINE_VERSION}" diff --git a/synth.metadata b/synth.metadata index a6d64a3b..23216c43 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "f2678ff5f8f5a0ee33924278b58e0a6e3122cb12" + "sha": "bf37628284d4e45c7ec5bb0063bc0f4a2fd9e048" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "89c849ba5013e45e8fb688b138f33c2ec6083dc5" + "sha": "ba9918cd22874245b55734f57470c719b577e591" } } ], From 65be3a83fb7af6bde79f6cfc46675c16ca999455 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Tue, 3 Nov 2020 09:36:26 -0800 Subject: [PATCH 204/662] docs: use the common readme template (#1095) --- .readme-partials.yaml | 398 ++++++++++++++++++++++++++++++++++++++++++ README.md | 192 ++++++++++++++------ samples/README.md | 2 +- synth.metadata | 56 +----- synth.py | 4 +- 5 files changed, 543 insertions(+), 109 deletions(-) create mode 100644 .readme-partials.yaml diff --git a/.readme-partials.yaml b/.readme-partials.yaml new file mode 100644 index 00000000..ddcf3b21 --- /dev/null +++ b/.readme-partials.yaml @@ -0,0 +1,398 @@ +introduction: |- + This is Google's officially supported [node.js](http://nodejs.org/) client library for using OAuth 2.0 authorization and authentication with Google APIs. +body: |- + ## Ways to authenticate + This library provides a variety of ways to authenticate to your Google services. + - [Application Default Credentials](#choosing-the-correct-credential-type-automatically) - Use Application Default Credentials when you use a single identity for all users in your application. Especially useful for applications running on Google Cloud. + - [OAuth 2](#oauth2) - Use OAuth2 when you need to perform actions on behalf of the end user. + - [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication. + - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. + + ## Application Default Credentials + This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. + + They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. + + #### Download your Service Account Credentials JSON file + + To use Application Default Credentials, You first need to download a set of JSON credentials for your project. Go to **APIs & Auth** > **Credentials** in the [Google Developers Console](https://console.cloud.google.com/) and select **Service account** from the **Add credentials** dropdown. + + > This file is your *only copy* of these credentials. It should never be + > committed with your source code, and should be stored securely. + + Once downloaded, store the path to this file in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. + + #### Enable the API you want to use + + Before making your API call, you must be sure the API you're calling has been enabled. Go to **APIs & Auth** > **APIs** in the [Google Developers Console](https://console.cloud.google.com/) and enable the APIs you'd like to call. For the example below, you must enable the `DNS API`. + + + #### Choosing the correct credential type automatically + + Rather than manually creating an OAuth2 client, JWT client, or Compute client, the auth library can create the correct credential type for you, depending upon the environment your code is running under. + + For example, a JWT auth client will be created when your code is running on your local developer machine, and a Compute client will be created when the same code is running on Google Cloud Platform. If you need a specific set of scopes, you can pass those in the form of a string or an array to the `GoogleAuth` constructor. + + The code below shows how to retrieve a default credential type, depending upon the runtime environment. + + ```js + const {GoogleAuth} = require('google-auth-library'); + + /** + * Instead of specifying the type of client you'd like to use (JWT, OAuth2, etc) + * this library will automatically choose the right client based on the environment. + */ + async function main() { + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform' + }); + const client = await auth.getClient(); + const projectId = await auth.getProjectId(); + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; + const res = await client.request({ url }); + console.log(res.data); + } + + main().catch(console.error); + ``` + + ## OAuth2 + + This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google Authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login). + + In the following examples, you may need a `CLIENT_ID`, `CLIENT_SECRET` and `REDIRECT_URL`. You can find these pieces of information by going to the [Developer Console](https://console.cloud.google.com/), clicking your project > APIs & auth > credentials. + + For more information about OAuth2 and how it works, [see here](https://developers.google.com/identity/protocols/OAuth2). + + #### A complete OAuth2 example + + Let's take a look at a complete example. + + ``` js + const {OAuth2Client} = require('google-auth-library'); + const http = require('http'); + const url = require('url'); + const open = require('open'); + const destroyer = require('server-destroy'); + + // Download your OAuth2 configuration from the Google + const keys = require('./oauth2.keys.json'); + + /** + * Start by acquiring a pre-authenticated oAuth2 client. + */ + async function main() { + const oAuth2Client = await getAuthenticatedClient(); + // Make a simple request to the People API using our pre-authenticated client. The `request()` method + // takes an GaxiosOptions object. Visit https://github.com/JustinBeckwith/gaxios. + const url = 'https://people.googleapis.com/v1/people/me?personFields=names'; + const res = await oAuth2Client.request({url}); + console.log(res.data); + + // After acquiring an access_token, you may want to check on the audience, expiration, + // or original scopes requested. You can do that with the `getTokenInfo` method. + const tokenInfo = await oAuth2Client.getTokenInfo( + oAuth2Client.credentials.access_token + ); + console.log(tokenInfo); + } + + /** + * Create a new OAuth2Client, and go through the OAuth2 content + * workflow. Return the full client to the callback. + */ + function getAuthenticatedClient() { + return new Promise((resolve, reject) => { + // create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file, + // which should be downloaded from the Google Developers Console. + const oAuth2Client = new OAuth2Client( + keys.web.client_id, + keys.web.client_secret, + keys.web.redirect_uris[0] + ); + + // Generate the url that will be used for the consent dialog. + const authorizeUrl = oAuth2Client.generateAuthUrl({ + access_type: 'offline', + scope: 'https://www.googleapis.com/auth/userinfo.profile', + }); + + // Open an http server to accept the oauth callback. In this simple example, the + // only request to our webserver is to /oauth2callback?code= + const server = http + .createServer(async (req, res) => { + try { + if (req.url.indexOf('/oauth2callback') > -1) { + // acquire the code from the querystring, and close the web server. + const qs = new url.URL(req.url, 'http://localhost:3000') + .searchParams; + const code = qs.get('code'); + console.log(`Code is ${code}`); + res.end('Authentication successful! Please return to the console.'); + server.destroy(); + + // Now that we have the code, use that to acquire tokens. + const r = await oAuth2Client.getToken(code); + // Make sure to set the credentials on the OAuth2 client. + oAuth2Client.setCredentials(r.tokens); + console.info('Tokens acquired.'); + resolve(oAuth2Client); + } + } catch (e) { + reject(e); + } + }) + .listen(3000, () => { + // open the browser to the authorize url to start the workflow + open(authorizeUrl, {wait: false}).then(cp => cp.unref()); + }); + destroyer(server); + }); + } + + main().catch(console.error); + ``` + + #### Handling token events + + This library will automatically obtain an `access_token`, and automatically refresh the `access_token` if a `refresh_token` is present. The `refresh_token` is only returned on the [first authorization](https://github.com/googleapis/google-api-nodejs-client/issues/750#issuecomment-304521450), so if you want to make sure you store it safely. An easy way to make sure you always store the most recent tokens is to use the `tokens` event: + + ```js + const client = await auth.getClient(); + + client.on('tokens', (tokens) => { + if (tokens.refresh_token) { + // store the refresh_token in my database! + console.log(tokens.refresh_token); + } + console.log(tokens.access_token); + }); + + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; + const res = await client.request({ url }); + // The `tokens` event would now be raised if this was the first request + ``` + + #### Retrieve access token + With the code returned, you can ask for an access token as shown below: + + ``` js + const tokens = await oauth2Client.getToken(code); + // Now tokens contains an access_token and an optional refresh_token. Save them. + oauth2Client.setCredentials(tokens); + ``` + + #### Obtaining a new Refresh Token + If you need to obtain a new `refresh_token`, ensure the call to `generateAuthUrl` sets the `access_type` to `offline`. The refresh token will only be returned for the first authorization by the user. To force consent, set the `prompt` property to `consent`: + + ```js + // Generate the url that will be used for the consent dialog. + const authorizeUrl = oAuth2Client.generateAuthUrl({ + // To get a refresh token, you MUST set access_type to `offline`. + access_type: 'offline', + // set the appropriate scopes + scope: 'https://www.googleapis.com/auth/userinfo.profile', + // A refresh token is only returned the first time the user + // consents to providing access. For illustration purposes, + // setting the prompt to 'consent' will force this consent + // every time, forcing a refresh_token to be returned. + prompt: 'consent' + }); + ``` + + #### Checking `access_token` information + After obtaining and storing an `access_token`, at a later time you may want to go check the expiration date, + original scopes, or audience for the token. To get the token info, you can use the `getTokenInfo` method: + + ```js + // after acquiring an oAuth2Client... + const tokenInfo = await oAuth2Client.getTokenInfo('my-access-token'); + + // take a look at the scopes originally provisioned for the access token + console.log(tokenInfo.scopes); + ``` + + This method will throw if the token is invalid. + + #### OAuth2 with Installed Apps (Electron) + If you're authenticating with OAuth2 from an installed application (like Electron), you may not want to embed your `client_secret` inside of the application sources. To work around this restriction, you can choose the `iOS` application type when creating your OAuth2 credentials in the [Google Developers console](https://console.cloud.google.com/): + + ![application type](https://user-images.githubusercontent.com/534619/36553844-3f9a863c-17b2-11e8-904a-29f6cd5f807a.png) + + If using the `iOS` type, when creating the OAuth2 client you won't need to pass a `client_secret` into the constructor: + ```js + const oAuth2Client = new OAuth2Client({ + clientId: , + redirectUri: + }); + ``` + + ## JSON Web Tokens + The Google Developers Console provides a `.json` file that you can use to configure a JWT auth client and authenticate your requests, for example when using a service account. + + ``` js + const {JWT} = require('google-auth-library'); + const keys = require('./jwt.keys.json'); + + async function main() { + const client = new JWT({ + email: keys.client_email, + key: keys.private_key, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; + const res = await client.request({url}); + console.log(res.data); + } + + main().catch(console.error); + ``` + + The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/jwt.js). + + #### Loading credentials from environment variables + Instead of loading credentials from a key file, you can also provide them using an environment variable and the `GoogleAuth.fromJSON()` method. This is particularly convenient for systems that deploy directly from source control (Heroku, App Engine, etc). + + Start by exporting your credentials: + + ``` + $ export CREDS='{ + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "your-private-key-id", + "private_key": "your-private-key", + "client_email": "your-client-email", + "client_id": "your-client-id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "your-cert-url" + }' + ``` + Now you can create a new client from the credentials: + + ```js + const {auth} = require('google-auth-library'); + + // load the environment variable with our keys + const keysEnvVar = process.env['CREDS']; + if (!keysEnvVar) { + throw new Error('The $CREDS environment variable was not found!'); + } + const keys = JSON.parse(keysEnvVar); + + async function main() { + // load the JWT or UserRefreshClient from the keys + const client = auth.fromJSON(keys); + client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; + const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; + const res = await client.request({url}); + console.log(res.data); + } + + main().catch(console.error); + ``` + + #### Using a Proxy + You can set the `HTTPS_PROXY` or `https_proxy` environment variables to proxy HTTPS requests. When `HTTPS_PROXY` or `https_proxy` are set, they will be used to proxy SSL requests that do not have an explicit proxy configuration option present. + + ## Compute + If your application is running on Google Cloud Platform, you can authenticate using the default service account or by specifying a specific service account. + + **Note**: In most cases, you will want to use [Application Default Credentials](#choosing-the-correct-credential-type-automatically). Direct use of the `Compute` class is for very specific scenarios. + + ``` js + const {auth, Compute} = require('google-auth-library'); + + async function main() { + const client = new Compute({ + // Specifying the service account email is optional. + serviceAccountEmail: 'my-service-account@example.com' + }); + const projectId = await auth.getProjectId(); + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; + const res = await client.request({url}); + console.log(res.data); + } + + main().catch(console.error); + ``` + + ## Working with ID Tokens + ### Fetching ID Tokens + If your application is running on Cloud Run or Cloud Functions, or using Cloud Identity-Aware + Proxy (IAP), you will need to fetch an ID token to access your application. For + this, use the method `getIdTokenClient` on the `GoogleAuth` client. + + For invoking Cloud Run services, your service account will need the + [`Cloud Run Invoker`](https://cloud.google.com/run/docs/authenticating/service-to-service) + IAM permission. + + For invoking Cloud Functions, your service account will need the + [`Function Invoker`](https://cloud.google.com/functions/docs/securing/authenticating#function-to-function) + IAM permission. + + ``` js + // Make a request to a protected Cloud Run service. + const {GoogleAuth} = require('google-auth-library'); + + async function main() { + const url = 'https://cloud-run-1234-uc.a.run.app'; + const auth = new GoogleAuth(); + const client = await auth.getIdTokenClient(url); + const res = await client.request({url}); + console.log(res.data); + } + + main().catch(console.error); + ``` + + A complete example can be found in [`samples/idtokens-serverless.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). + + For invoking Cloud Identity-Aware Proxy, you will need to pass the Client ID + used when you set up your protected resource as the target audience. + + ``` js + // Make a request to a protected Cloud Identity-Aware Proxy (IAP) resource + const {GoogleAuth} = require('google-auth-library'); + + async function main() + const targetAudience = 'iap-client-id'; + const url = 'https://iap-url.com'; + const auth = new GoogleAuth(); + const client = await auth.getIdTokenClient(targetAudience); + const res = await client.request({url}); + console.log(res.data); + } + + main().catch(console.error); + ``` + + A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). + + ### Verifying ID Tokens + + If you've [secured your IAP app with signed headers](https://cloud.google.com/iap/docs/signed-headers-howto), + you can use this library to verify the IAP header: + + ```js + const {OAuth2Client} = require('google-auth-library'); + // Expected audience for App Engine. + const expectedAudience = `/projects/your-project-number/apps/your-project-id`; + // IAP issuer + const issuers = ['https://cloud.google.com/iap']; + // Verify the token. OAuth2Client throws an Error if verification fails + const oAuth2Client = new OAuth2Client(); + const response = await oAuth2Client.getIapCerts(); + const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( + idToken, + response.pubkeys, + expectedAudience, + issuers + ); + + // Print out the info contained in the IAP ID token + console.log(ticket) + ``` + + A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). diff --git a/README.md b/README.md index f29c488a..d89b94a7 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,49 @@ -Google Inc. logo +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." +Google Cloud Platform logo -# Google Auth Library +# [Google Auth Library: Node.js Client](https://github.com/googleapis/google-auth-library-nodejs) -[![npm version][npmimg]][npm] -[![codecov][codecov-image]][codecov-url] -[![Dependencies][david-dm-img]][david-dm] -[![Known Vulnerabilities][snyk-image]][snyk-url] +[![release level](https://img.shields.io/badge/release%20level-general%20availability%20%28GA%29-brightgreen.svg?style=flat)](https://cloud.google.com/terms/launch-stages) +[![npm version](https://img.shields.io/npm/v/google-auth-library.svg)](https://www.npmjs.org/package/google-auth-library) +[![codecov](https://img.shields.io/codecov/c/github/googleapis/google-auth-library-nodejs/master.svg?style=flat)](https://codecov.io/gh/googleapis/google-auth-library-nodejs) -This is Google's officially supported [node.js][node] client library for using OAuth 2.0 authorization and authentication with Google APIs. -## Installation -This library is distributed on `npm`. To add it as a dependency, run the following command: -``` sh -$ npm install google-auth-library + +This is Google's officially supported [node.js](http://nodejs.org/) client library for using OAuth 2.0 authorization and authentication with Google APIs. + + +A comprehensive list of changes in each version may be found in +[the CHANGELOG](https://github.com/googleapis/google-auth-library-nodejs/blob/master/CHANGELOG.md). + +* [Google Auth Library Node.js Client API Reference][client-docs] +* [Google Auth Library Documentation][product-docs] +* [github.com/googleapis/google-auth-library-nodejs](https://github.com/googleapis/google-auth-library-nodejs) + +Read more about the client libraries for Cloud APIs, including the older +Google APIs Client Libraries, in [Client Libraries Explained][explained]. + +[explained]: https://cloud.google.com/apis/docs/client-libraries-explained + +**Table of contents:** + + +* [Quickstart](#quickstart) + + * [Installing the client library](#installing-the-client-library) + +* [Samples](#samples) +* [Versioning](#versioning) +* [Contributing](#contributing) +* [License](#license) + +## Quickstart + +### Installing the client library + +```bash +npm install google-auth-library ``` ## Ways to authenticate @@ -24,13 +54,13 @@ This library provides a variety of ways to authenticate to your Google services. - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. ## Application Default Credentials -This library provides an implementation of [Application Default Credentials][] for Node.js. The [Application Default Credentials][] provide a simple way to get authorization credentials for use in calling Google APIs. +This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. #### Download your Service Account Credentials JSON file -To use Application Default Credentials, You first need to download a set of JSON credentials for your project. Go to **APIs & Auth** > **Credentials** in the [Google Developers Console][devconsole] and select **Service account** from the **Add credentials** dropdown. +To use Application Default Credentials, You first need to download a set of JSON credentials for your project. Go to **APIs & Auth** > **Credentials** in the [Google Developers Console](https://console.cloud.google.com/) and select **Service account** from the **Add credentials** dropdown. > This file is your *only copy* of these credentials. It should never be > committed with your source code, and should be stored securely. @@ -39,7 +69,7 @@ Once downloaded, store the path to this file in the `GOOGLE_APPLICATION_CREDENTI #### Enable the API you want to use -Before making your API call, you must be sure the API you're calling has been enabled. Go to **APIs & Auth** > **APIs** in the [Google Developers Console][devconsole] and enable the APIs you'd like to call. For the example below, you must enable the `DNS API`. +Before making your API call, you must be sure the API you're calling has been enabled. Go to **APIs & Auth** > **APIs** in the [Google Developers Console](https://console.cloud.google.com/) and enable the APIs you'd like to call. For the example below, you must enable the `DNS API`. #### Choosing the correct credential type automatically @@ -54,9 +84,9 @@ The code below shows how to retrieve a default credential type, depending upon t const {GoogleAuth} = require('google-auth-library'); /** - * Instead of specifying the type of client you'd like to use (JWT, OAuth2, etc) - * this library will automatically choose the right client based on the environment. - */ +* Instead of specifying the type of client you'd like to use (JWT, OAuth2, etc) +* this library will automatically choose the right client based on the environment. +*/ async function main() { const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform' @@ -73,11 +103,11 @@ main().catch(console.error); ## OAuth2 -This library comes with an [OAuth2][oauth] client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google Authorization and Authentication documentation][authdocs]. +This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google Authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login). -In the following examples, you may need a `CLIENT_ID`, `CLIENT_SECRET` and `REDIRECT_URL`. You can find these pieces of information by going to the [Developer Console][devconsole], clicking your project > APIs & auth > credentials. +In the following examples, you may need a `CLIENT_ID`, `CLIENT_SECRET` and `REDIRECT_URL`. You can find these pieces of information by going to the [Developer Console](https://console.cloud.google.com/), clicking your project > APIs & auth > credentials. -For more information about OAuth2 and how it works, [see here][oauth]. +For more information about OAuth2 and how it works, [see here](https://developers.google.com/identity/protocols/OAuth2). #### A complete OAuth2 example @@ -94,8 +124,8 @@ const destroyer = require('server-destroy'); const keys = require('./oauth2.keys.json'); /** - * Start by acquiring a pre-authenticated oAuth2 client. - */ +* Start by acquiring a pre-authenticated oAuth2 client. +*/ async function main() { const oAuth2Client = await getAuthenticatedClient(); // Make a simple request to the People API using our pre-authenticated client. The `request()` method @@ -113,9 +143,9 @@ async function main() { } /** - * Create a new OAuth2Client, and go through the OAuth2 content - * workflow. Return the full client to the callback. - */ +* Create a new OAuth2Client, and go through the OAuth2 content +* workflow. Return the full client to the callback. +*/ function getAuthenticatedClient() { return new Promise((resolve, reject) => { // create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file, @@ -230,9 +260,9 @@ console.log(tokenInfo.scopes); This method will throw if the token is invalid. #### OAuth2 with Installed Apps (Electron) -If you're authenticating with OAuth2 from an installed application (like Electron), you may not want to embed your `client_secret` inside of the application sources. To work around this restriction, you can choose the `iOS` application type when creating your OAuth2 credentials in the [Google Developers console][devconsole]: +If you're authenticating with OAuth2 from an installed application (like Electron), you may not want to embed your `client_secret` inside of the application sources. To work around this restriction, you can choose the `iOS` application type when creating your OAuth2 credentials in the [Google Developers console](https://console.cloud.google.com/): -![application type][apptype] +![application type](https://user-images.githubusercontent.com/534619/36553844-3f9a863c-17b2-11e8-904a-29f6cd5f807a.png) If using the `iOS` type, when creating the OAuth2 client you won't need to pass a `client_secret` into the constructor: ```js @@ -412,34 +442,94 @@ console.log(ticket) A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). -## Questions/problems? -* Ask your development related questions on [Stack Overflow][stackoverflow]. -* If you've found an bug/issue, please [file it on GitHub][bugs]. +## Samples + +Samples are in the [`samples/`](https://github.com/googleapis/google-auth-library-nodejs/tree/master/samples) directory. The samples' `README.md` +has instructions for running the samples. + +| Sample | Source Code | Try it | +| --------------------------- | --------------------------------- | ------ | +| Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/adc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/adc.js,samples/README.md) | +| Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/compute.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) | +| Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/credentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/credentials.js,samples/README.md) | +| Headers | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/headers.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/headers.js,samples/README.md) | +| ID Tokens for Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) | +| ID Tokens for Serverless | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) | +| Jwt | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/jwt.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/jwt.js,samples/README.md) | +| Keepalive | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/keepalive.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/keepalive.js,samples/README.md) | +| Keyfile | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/keyfile.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/keyfile.js,samples/README.md) | +| Oauth2-code Verifier | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2-codeVerifier.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2-codeVerifier.js,samples/README.md) | +| Oauth2 | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2.js,samples/README.md) | +| Sign Blob | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/signBlob.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlob.js,samples/README.md) | +| Verifying ID Tokens from Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken-iap.js,samples/README.md) | +| Verify Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken.js,samples/README.md) | + + + +The [Google Auth Library Node.js Client API Reference][client-docs] documentation +also contains samples. + +## Supported Node.js Versions + +Our client libraries follow the [Node.js release schedule](https://nodejs.org/en/about/releases/). +Libraries are compatible with all current _active_ and _maintenance_ versions of +Node.js. + +Client libraries targetting some end-of-life versions of Node.js are available, and +can be installed via npm [dist-tags](https://docs.npmjs.com/cli/dist-tag). +The dist-tags follow the naming convention `legacy-(version)`. + +_Legacy Node.js versions are supported as a best effort:_ + +* Legacy versions will not be tested in continuous integration. +* Some security patches may not be able to be backported. +* Dependencies will not be kept up-to-date, and features will not be backported. + +#### Legacy tags available + +* `legacy-8`: install client libraries from this dist-tag for versions + compatible with Node.js 8. + +## Versioning + +This library follows [Semantic Versioning](http://semver.org/). + + +This library is considered to be **General Availability (GA)**. This means it +is stable; the code surface will not change in backwards-incompatible ways +unless absolutely necessary (e.g. because of critical security issues) or with +an extensive deprecation period. Issues and requests against **GA** libraries +are addressed with the highest priority. + + + + + +More Information: [Google Cloud Platform Launch Stages][launch_stages] + +[launch_stages]: https://cloud.google.com/terms/launch-stages ## Contributing -See [CONTRIBUTING][contributing]. +Contributions welcome! See the [Contributing Guide](https://github.com/googleapis/google-auth-library-nodejs/blob/master/CONTRIBUTING.md). + +Please note that this `README.md`, the `samples/README.md`, +and a variety of configuration files in this repository (including `.nycrc` and `tsconfig.json`) +are generated from a central template. To edit one of these files, make an edit +to its template in this +[directory](https://github.com/googleapis/synthtool/tree/master/synthtool/gcp/templates/node_library). ## License -This library is licensed under Apache 2.0. Full license text is available in [LICENSE][copying]. - -[Application Default Credentials]: https://cloud.google.com/docs/authentication/getting-started -[apptype]: https://user-images.githubusercontent.com/534619/36553844-3f9a863c-17b2-11e8-904a-29f6cd5f807a.png -[authdocs]: https://developers.google.com/accounts/docs/OAuth2Login -[bugs]: https://github.com/googleapis/google-auth-library-nodejs/issues -[codecov-image]: https://codecov.io/gh/googleapis/google-auth-library-nodejs/branch/master/graph/badge.svg -[codecov-url]: https://codecov.io/gh/googleapis/google-auth-library-nodejs -[contributing]: https://github.com/googleapis/google-auth-library-nodejs/blob/master/CONTRIBUTING.md -[copying]: https://github.com/googleapis/google-auth-library-nodejs/tree/master/LICENSE -[david-dm-img]: https://david-dm.org/googleapis/google-auth-library-nodejs/status.svg -[david-dm]: https://david-dm.org/googleapis/google-auth-library-nodejs -[node]: http://nodejs.org/ -[npmimg]: https://img.shields.io/npm/v/google-auth-library.svg -[npm]: https://www.npmjs.org/package/google-auth-library -[oauth]: https://developers.google.com/identity/protocols/OAuth2 -[snyk-image]: https://snyk.io/test/github/googleapis/google-auth-library-nodejs/badge.svg -[snyk-url]: https://snyk.io/test/github/googleapis/google-auth-library-nodejs -[stackoverflow]: http://stackoverflow.com/questions/tagged/google-auth-library-nodejs -[devconsole]: https://console.cloud.google.com/ +Apache Version 2.0 + +See [LICENSE](https://github.com/googleapis/google-auth-library-nodejs/blob/master/LICENSE) + +[client-docs]: https://googleapis.dev/nodejs/google-auth-library/latest +[product-docs]: https://cloud.google.com/docs/authentication/ +[shell_img]: https://gstatic.com/cloudssh/images/open-btn.png +[projects]: https://console.cloud.google.com/project +[billing]: https://support.google.com/cloud/answer/6293499#enable-billing + +[auth]: https://cloud.google.com/docs/authentication/getting-started diff --git a/samples/README.md b/samples/README.md index 40571df3..c4f4e5be 100644 --- a/samples/README.md +++ b/samples/README.md @@ -6,7 +6,7 @@ [![Open in Cloud Shell][shell_img]][shell_link] - +This is Google's officially supported [node.js](http://nodejs.org/) client library for using OAuth 2.0 authorization and authentication with Google APIs. ## Table of Contents diff --git a/synth.metadata b/synth.metadata index 23216c43..8f26516f 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,8 +3,8 @@ { "git": { "name": ".", - "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "bf37628284d4e45c7ec5bb0063bc0f4a2fd9e048" + "remote": "git@github.com:googleapis/google-auth-library-nodejs.git", + "sha": "502a3d954bb13dfcf5a22216504e858135382a83" } }, { @@ -14,57 +14,5 @@ "sha": "ba9918cd22874245b55734f57470c719b577e591" } } - ], - "generatedFiles": [ - ".eslintignore", - ".eslintrc.json", - ".gitattributes", - ".github/ISSUE_TEMPLATE/bug_report.md", - ".github/ISSUE_TEMPLATE/feature_request.md", - ".github/ISSUE_TEMPLATE/support_request.md", - ".github/PULL_REQUEST_TEMPLATE.md", - ".github/release-please.yml", - ".github/workflows/ci.yaml", - ".kokoro/.gitattributes", - ".kokoro/common.cfg", - ".kokoro/continuous/node10/common.cfg", - ".kokoro/continuous/node10/docs.cfg", - ".kokoro/continuous/node10/test.cfg", - ".kokoro/continuous/node12/common.cfg", - ".kokoro/continuous/node12/lint.cfg", - ".kokoro/continuous/node12/samples-test.cfg", - ".kokoro/continuous/node12/system-test.cfg", - ".kokoro/continuous/node12/test.cfg", - ".kokoro/docs.sh", - ".kokoro/lint.sh", - ".kokoro/populate-secrets.sh", - ".kokoro/presubmit/node10/common.cfg", - ".kokoro/presubmit/node12/common.cfg", - ".kokoro/presubmit/node12/samples-test.cfg", - ".kokoro/presubmit/node12/system-test.cfg", - ".kokoro/presubmit/node12/test.cfg", - ".kokoro/publish.sh", - ".kokoro/release/docs-devsite.cfg", - ".kokoro/release/docs-devsite.sh", - ".kokoro/release/docs.cfg", - ".kokoro/release/docs.sh", - ".kokoro/release/publish.cfg", - ".kokoro/samples-test.sh", - ".kokoro/system-test.sh", - ".kokoro/test.bat", - ".kokoro/test.sh", - ".kokoro/trampoline.sh", - ".kokoro/trampoline_v2.sh", - ".mocharc.js", - ".nycrc", - ".prettierignore", - ".prettierrc.js", - ".trampolinerc", - "CODE_OF_CONDUCT.md", - "CONTRIBUTING.md", - "LICENSE", - "api-extractor.json", - "renovate.json", - "samples/README.md" ] } \ No newline at end of file diff --git a/synth.py b/synth.py index 84d0f33e..294439b0 100644 --- a/synth.py +++ b/synth.py @@ -8,6 +8,4 @@ common_templates = gcp.CommonTemplates() templates = common_templates.node_library() -s.copy(templates, excludes=["README.md"]) -node.install() -node.fix() +s.copy(templates) From a5682b825b2716f983669c919ca8fef9ebbc3004 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 4 Nov 2020 07:02:01 -0800 Subject: [PATCH 205/662] chore: start tracking obsolete files (#1096) --- synth.metadata | 57 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/synth.metadata b/synth.metadata index 8f26516f..4f2a1e8d 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,8 +3,8 @@ { "git": { "name": ".", - "remote": "git@github.com:googleapis/google-auth-library-nodejs.git", - "sha": "502a3d954bb13dfcf5a22216504e858135382a83" + "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", + "sha": "65be3a83fb7af6bde79f6cfc46675c16ca999455" } }, { @@ -14,5 +14,58 @@ "sha": "ba9918cd22874245b55734f57470c719b577e591" } } + ], + "generatedFiles": [ + ".eslintignore", + ".eslintrc.json", + ".gitattributes", + ".github/ISSUE_TEMPLATE/bug_report.md", + ".github/ISSUE_TEMPLATE/feature_request.md", + ".github/ISSUE_TEMPLATE/support_request.md", + ".github/PULL_REQUEST_TEMPLATE.md", + ".github/release-please.yml", + ".github/workflows/ci.yaml", + ".kokoro/.gitattributes", + ".kokoro/common.cfg", + ".kokoro/continuous/node10/common.cfg", + ".kokoro/continuous/node10/docs.cfg", + ".kokoro/continuous/node10/test.cfg", + ".kokoro/continuous/node12/common.cfg", + ".kokoro/continuous/node12/lint.cfg", + ".kokoro/continuous/node12/samples-test.cfg", + ".kokoro/continuous/node12/system-test.cfg", + ".kokoro/continuous/node12/test.cfg", + ".kokoro/docs.sh", + ".kokoro/lint.sh", + ".kokoro/populate-secrets.sh", + ".kokoro/presubmit/node10/common.cfg", + ".kokoro/presubmit/node12/common.cfg", + ".kokoro/presubmit/node12/samples-test.cfg", + ".kokoro/presubmit/node12/system-test.cfg", + ".kokoro/presubmit/node12/test.cfg", + ".kokoro/publish.sh", + ".kokoro/release/docs-devsite.cfg", + ".kokoro/release/docs-devsite.sh", + ".kokoro/release/docs.cfg", + ".kokoro/release/docs.sh", + ".kokoro/release/publish.cfg", + ".kokoro/samples-test.sh", + ".kokoro/system-test.sh", + ".kokoro/test.bat", + ".kokoro/test.sh", + ".kokoro/trampoline.sh", + ".kokoro/trampoline_v2.sh", + ".mocharc.js", + ".nycrc", + ".prettierignore", + ".prettierrc.js", + ".trampolinerc", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "api-extractor.json", + "renovate.json", + "samples/README.md" ] } \ No newline at end of file From a81765be7d0f2152af20a66d12821d27c4fd6b4f Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 25 Nov 2020 08:34:09 -0800 Subject: [PATCH 206/662] docs: spelling correction for "targetting" (#1101) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/b1c0ae50-0eb0-40ae-94e1-24ab7c3527c2/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/15013eff642a7e7e855aed5a29e6e83c39beba2a --- README.md | 2 +- synth.metadata | 57 ++------------------------------------------------ 2 files changed, 3 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index d89b94a7..68e4dde1 100644 --- a/README.md +++ b/README.md @@ -476,7 +476,7 @@ Our client libraries follow the [Node.js release schedule](https://nodejs.org/en Libraries are compatible with all current _active_ and _maintenance_ versions of Node.js. -Client libraries targetting some end-of-life versions of Node.js are available, and +Client libraries targeting some end-of-life versions of Node.js are available, and can be installed via npm [dist-tags](https://docs.npmjs.com/cli/dist-tag). The dist-tags follow the naming convention `legacy-(version)`. diff --git a/synth.metadata b/synth.metadata index 4f2a1e8d..61327138 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,68 +4,15 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "65be3a83fb7af6bde79f6cfc46675c16ca999455" + "sha": "a5682b825b2716f983669c919ca8fef9ebbc3004" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "ba9918cd22874245b55734f57470c719b577e591" + "sha": "15013eff642a7e7e855aed5a29e6e83c39beba2a" } } - ], - "generatedFiles": [ - ".eslintignore", - ".eslintrc.json", - ".gitattributes", - ".github/ISSUE_TEMPLATE/bug_report.md", - ".github/ISSUE_TEMPLATE/feature_request.md", - ".github/ISSUE_TEMPLATE/support_request.md", - ".github/PULL_REQUEST_TEMPLATE.md", - ".github/release-please.yml", - ".github/workflows/ci.yaml", - ".kokoro/.gitattributes", - ".kokoro/common.cfg", - ".kokoro/continuous/node10/common.cfg", - ".kokoro/continuous/node10/docs.cfg", - ".kokoro/continuous/node10/test.cfg", - ".kokoro/continuous/node12/common.cfg", - ".kokoro/continuous/node12/lint.cfg", - ".kokoro/continuous/node12/samples-test.cfg", - ".kokoro/continuous/node12/system-test.cfg", - ".kokoro/continuous/node12/test.cfg", - ".kokoro/docs.sh", - ".kokoro/lint.sh", - ".kokoro/populate-secrets.sh", - ".kokoro/presubmit/node10/common.cfg", - ".kokoro/presubmit/node12/common.cfg", - ".kokoro/presubmit/node12/samples-test.cfg", - ".kokoro/presubmit/node12/system-test.cfg", - ".kokoro/presubmit/node12/test.cfg", - ".kokoro/publish.sh", - ".kokoro/release/docs-devsite.cfg", - ".kokoro/release/docs-devsite.sh", - ".kokoro/release/docs.cfg", - ".kokoro/release/docs.sh", - ".kokoro/release/publish.cfg", - ".kokoro/samples-test.sh", - ".kokoro/system-test.sh", - ".kokoro/test.bat", - ".kokoro/test.sh", - ".kokoro/trampoline.sh", - ".kokoro/trampoline_v2.sh", - ".mocharc.js", - ".nycrc", - ".prettierignore", - ".prettierrc.js", - ".trampolinerc", - "CODE_OF_CONDUCT.md", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "api-extractor.json", - "renovate.json", - "samples/README.md" ] } \ No newline at end of file From bf5ced3baf44ed122c98bb2ce4c751cbafd1b97c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 3 Dec 2020 18:23:21 +0100 Subject: [PATCH 207/662] chore(deps): update dependency execa to v5 (#1104) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc810fda..949aa171 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "c8": "^7.0.0", "chai": "^4.2.0", "codecov": "^3.0.2", - "execa": "^4.0.0", + "execa": "^5.0.0", "gts": "^2.0.0", "is-docker": "^2.0.0", "karma": "^5.0.0", From 67b0cc3077860a1583bcf18ce50aeff58bbb5496 Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Thu, 17 Dec 2020 17:39:22 -0800 Subject: [PATCH 208/662] fix: move accessToken to headers instead of parameter (#1108) --- src/auth/oauth2client.ts | 2 +- test/test.oauth2.ts | 19 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 046863f5..47809098 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -1018,9 +1018,9 @@ export class OAuth2Client extends AuthClient { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Bearer ${accessToken}`, }, url: OAuth2Client.GOOGLE_TOKEN_INFO_URL, - data: querystring.stringify({access_token: accessToken}), }); const info = Object.assign( { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index e53fbcaa..62fd416a 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1322,18 +1322,13 @@ describe('oauth2', () => { expires_in: 1234, }; - const scope = nock(baseUrl) - .post( - '/tokeninfo', - qs.stringify({ - access_token: accessToken, - }), - { - reqheaders: { - 'content-type': 'application/x-www-form-urlencoded', - }, - } - ) + const scope = nock(baseUrl, { + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded', + authorization: `Bearer ${accessToken}`, + }, + }) + .post('/tokeninfo', () => true) .reply(200, tokenInfo); const info = await client.getTokenInfo(accessToken); From 95d76e2c09f1011d88e0982d1d94eef03036933e Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 22 Dec 2020 11:42:20 -0800 Subject: [PATCH 209/662] docs: add instructions for authenticating for system tests (#1110) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/f858a143-daac-4e50-b9ae-219abe9981ce/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/363fe305e9ce34a6cd53951c6ee5f997094b54ee --- CONTRIBUTING.md | 13 +++++++++++-- README.md | 3 +-- synth.metadata | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6c4cf01..72c44cad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,13 @@ accept your pull requests. 1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling. 1. Submit a pull request. +### Before you begin + +1. [Select or create a Cloud Platform project][projects]. +1. [Set up authentication with a service account][auth] so you can access the + API from your local workstation. + + ## Running the tests 1. [Prepare your environment for Node.js setup][setup]. @@ -51,11 +58,9 @@ accept your pull requests. npm test # Run sample integration tests. - gcloud auth application-default login npm run samples-test # Run all system tests. - gcloud auth application-default login npm run system-test 1. Lint (and maybe fix) any changes: @@ -63,3 +68,7 @@ accept your pull requests. npm run fix [setup]: https://cloud.google.com/nodejs/docs/setup +[projects]: https://console.cloud.google.com/project +[billing]: https://support.google.com/cloud/answer/6293499#enable-billing + +[auth]: https://cloud.google.com/docs/authentication/getting-started \ No newline at end of file diff --git a/README.md b/README.md index 68e4dde1..da54ef90 100644 --- a/README.md +++ b/README.md @@ -445,8 +445,7 @@ A complete example can be found in [`samples/verifyIdToken-iap.js`](https://gith ## Samples -Samples are in the [`samples/`](https://github.com/googleapis/google-auth-library-nodejs/tree/master/samples) directory. The samples' `README.md` -has instructions for running the samples. +Samples are in the [`samples/`](https://github.com/googleapis/google-auth-library-nodejs/tree/master/samples) directory. Each sample's `README.md` has instructions for running its sample. | Sample | Source Code | Try it | | --------------------------- | --------------------------------- | ------ | diff --git a/synth.metadata b/synth.metadata index 61327138..14382daa 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "a5682b825b2716f983669c919ca8fef9ebbc3004" + "sha": "67b0cc3077860a1583bcf18ce50aeff58bbb5496" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "15013eff642a7e7e855aed5a29e6e83c39beba2a" + "sha": "363fe305e9ce34a6cd53951c6ee5f997094b54ee" } } ] From 92cfb338486cbdc6b663076bce9a2212753d9257 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 7 Jan 2021 11:46:43 -0800 Subject: [PATCH 210/662] chore: release 6.1.4 (#1109) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb98e95..60cfecae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.1.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.3...v6.1.4) (2020-12-22) + + +### Bug Fixes + +* move accessToken to headers instead of parameter ([#1108](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1108)) ([67b0cc3](https://www.github.com/googleapis/google-auth-library-nodejs/commit/67b0cc3077860a1583bcf18ce50aeff58bbb5496)) + ### [6.1.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.2...v6.1.3) (2020-10-22) diff --git a/package.json b/package.json index 949aa171..ce8d1d1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.1.3", + "version": "6.1.4", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index e4fa34e5..2ca0a542 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.1.3", + "google-auth-library": "^6.1.4", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 3e6035a32cdd95b36b2b8ef073cfb0ca8757e231 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 14 Jan 2021 18:32:57 +0100 Subject: [PATCH 211/662] chore(deps): update dependency karma to v6 (#1115) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce8d1d1a..9b5e0db7 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "execa": "^5.0.0", "gts": "^2.0.0", "is-docker": "^2.0.0", - "karma": "^5.0.0", + "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^2.0.0", "karma-firefox-launcher": "^2.0.0", From c2ead4cc7650f100b883c9296fce628f17085992 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 22 Jan 2021 11:49:30 -0800 Subject: [PATCH 212/662] fix: support PEM and p12 when using factory (#1120) --- src/auth/googleauth.ts | 18 ++++++++++++--- test/test.googleauth.ts | 49 +++++++++++++++++++++++++++++++++++++++++ test/test.jwt.ts | 2 +- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 29ff5d95..437677ab 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -518,9 +518,21 @@ export class GoogleAuth { .on('data', chunk => (s += chunk)) .on('end', () => { try { - const data = JSON.parse(s); - const r = this._cacheClientFromJSON(data, options); - return resolve(r); + 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 JWT({ + ...this.clientOptions, + keyFile: this.keyFilename, + }); + this.cachedCredential = client; + return resolve(client); + } } catch (err) { return reject(err); } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 109998de..e1039ba3 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -16,6 +16,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import * as child_process from 'child_process'; import * as crypto from 'crypto'; +import {CredentialRequest} from '../src/auth/credentials'; import * as fs from 'fs'; import { BASE_PATH, @@ -44,6 +45,8 @@ describe('googleauth', () => { const instancePath = `${BASE_PATH}/instance`; const svcAccountPath = `${instancePath}/service-accounts/?recursive=true`; const API_KEY = 'test-123'; + const PEM_PATH = './test/fixtures/private.pem'; + const P12_PATH = './test/fixtures/key.p12'; const STUB_PROJECT = 'my-awesome-project'; const ENDPOINT = '/events:report'; const RESPONSE_BODY = 'RESPONSE_BODY'; @@ -74,6 +77,11 @@ describe('googleauth', () => { 'gcloud', 'application_default_credentials.json' ); + function createGTokenMock(body: CredentialRequest) { + return nock('https://www.googleapis.com') + .post('/oauth2/v4/token') + .reply(200, body); + } describe('googleauth', () => { let auth: GoogleAuth; @@ -1524,4 +1532,45 @@ describe('googleauth', () => { .reply(200, {}); } }); + + // Allows a client to be instantiated from a certificate, + // See: https://github.com/googleapis/google-auth-library-nodejs/issues/808 + it('allows client to be instantiated from PEM key file', async () => { + const auth = new GoogleAuth({ + keyFile: PEM_PATH, + clientOptions: { + scopes: 'http://foo', + email: 'foo@serviceaccount.com', + subject: 'bar@subjectaccount.com', + }, + }); + const jwt = await auth.getClient(); + const scope = createGTokenMock({access_token: 'initial-access-token'}); + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual( + headers.Authorization, + 'Bearer initial-access-token' + ); + scope.done(); + assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); + }); + it('allows client to be instantiated from p12 key file', async () => { + const auth = new GoogleAuth({ + keyFile: P12_PATH, + clientOptions: { + scopes: 'http://foo', + email: 'foo@serviceaccount.com', + subject: 'bar@subjectaccount.com', + }, + }); + const jwt = await auth.getClient(); + const scope = createGTokenMock({access_token: 'initial-access-token'}); + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual( + headers.Authorization, + 'Bearer initial-access-token' + ); + scope.done(); + assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); + }); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index dd569a03..aa70ea44 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -118,8 +118,8 @@ describe('jwt', () => { scopes: 'http://foo', subject: 'bar@subjectaccount.com', }); - const scope = createGTokenMock({access_token: 'initial-access-token'}); + jwt.authorize(() => { scope.done(); assert.strictEqual('http://foo', jwt.gtoken!.scope); From 8786d15581912efe8a4371d3bfd798923e03626a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 11:30:21 -0800 Subject: [PATCH 213/662] chore: release 6.1.5 (#1121) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60cfecae..78449ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.1.5](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.4...v6.1.5) (2021-01-22) + + +### Bug Fixes + +* support PEM and p12 when using factory ([#1120](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1120)) ([c2ead4c](https://www.github.com/googleapis/google-auth-library-nodejs/commit/c2ead4cc7650f100b883c9296fce628f17085992)) + ### [6.1.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.3...v6.1.4) (2020-12-22) diff --git a/package.json b/package.json index 9b5e0db7..86dbbd84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.1.4", + "version": "6.1.5", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 2ca0a542..41889426 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.1.4", + "google-auth-library": "^6.1.5", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From aad043d20df3f1e44f56c58a21f15000b6fe970d Mon Sep 17 00:00:00 2001 From: Hideto Inamura Date: Wed, 27 Jan 2021 09:58:58 +0900 Subject: [PATCH 214/662] fix: call addSharedMetadataHeaders even when token has not expired (#1116) --- src/auth/oauth2client.ts | 2 +- test/test.refresh.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 47809098..20572d33 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -786,7 +786,7 @@ export class OAuth2Client extends AuthClient { const headers = { Authorization: thisCreds.token_type + ' ' + thisCreds.access_token, }; - return {headers}; + return {headers: this.addSharedMetadataHeaders(headers)}; } if (this.apiKey) { diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 8fd59cc5..2e06d7de 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -152,4 +152,41 @@ describe('refresh', () => { assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); req.done(); }); + + it('getRequestHeaders should populate x-goog-user-project header if quota_project_id present and token has not expired', async () => { + const stream = fs.createReadStream( + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + ); + const eagerRefreshThresholdMillis = 10; + const refresh = new UserRefreshClient({ + eagerRefreshThresholdMillis, + }); + await refresh.fromStream(stream); + refresh.credentials = { + access_token: 'woot', + refresh_token: 'jwt-placeholder', + expiry_date: new Date().getTime() + eagerRefreshThresholdMillis + 1000, + }; + const headers = await refresh.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + }); + + it('getRequestHeaders should populate x-goog-user-project header if quota_project_id present and token has expired', async () => { + const req = nock('https://oauth2.googleapis.com') + .post('/token') + .reply(200, {}); + const stream = fs.createReadStream( + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + ); + const refresh = new UserRefreshClient(); + await refresh.fromStream(stream); + refresh.credentials = { + access_token: 'woot', + refresh_token: 'jwt-placeholder', + expiry_date: new Date().getTime() - 1, + }; + const headers = await refresh.getRequestHeaders(); + assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + req.done(); + }); }); From c27dec9e07d4e6873fdc486d25ed7b50e9560ea6 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 26 Jan 2021 17:06:17 -0800 Subject: [PATCH 215/662] chore: release 6.1.6 (#1123) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78449ee9..bfc0dbcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [6.1.6](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.5...v6.1.6) (2021-01-27) + + +### Bug Fixes + +* call addSharedMetadataHeaders even when token has not expired ([#1116](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1116)) ([aad043d](https://www.github.com/googleapis/google-auth-library-nodejs/commit/aad043d20df3f1e44f56c58a21f15000b6fe970d)) + ### [6.1.5](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.4...v6.1.5) (2021-01-22) diff --git a/package.json b/package.json index 86dbbd84..a4c5954c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.1.5", + "version": "6.1.6", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 41889426..75269a2a 100644 --- a/samples/package.json +++ b/samples/package.json @@ -12,7 +12,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.1.5", + "google-auth-library": "^6.1.6", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 79355a7eef3acfca8536f0e6d8aab7c5f67b38fa Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 27 Jan 2021 08:40:23 -0800 Subject: [PATCH 216/662] refactor(nodejs): move build cop to flakybot (#1124) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/1ff854df-6525-4c07-b2b3-1ffce863e3f6/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/57c23fa5705499a4181095ced81f0ee0933b64f6 --- .kokoro/samples-test.sh | 6 +++--- .kokoro/system-test.sh | 6 +++--- .kokoro/test.sh | 6 +++--- .kokoro/trampoline_v2.sh | 2 +- synth.metadata | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index bab7ba4e..950f8483 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -39,14 +39,14 @@ if [ -f samples/package.json ]; then npm link ../ npm install cd .. - # If tests are running against master, configure Build Cop + # If tests are running against master, configure flakybot # to open issues on failures: if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml export MOCHA_REPORTER=xunit cleanup() { - chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop - $KOKORO_GFILE_DIR/linux_amd64/buildcop + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot } trap cleanup EXIT HUP fi diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 8a084004..319d1e0e 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -33,14 +33,14 @@ fi npm install -# If tests are running against master, configure Build Cop +# If tests are running against master, configure flakybot # to open issues on failures: if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml export MOCHA_REPORTER=xunit cleanup() { - chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop - $KOKORO_GFILE_DIR/linux_amd64/buildcop + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot } trap cleanup EXIT HUP fi diff --git a/.kokoro/test.sh b/.kokoro/test.sh index 5be385fe..5d6383fc 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -21,14 +21,14 @@ export NPM_CONFIG_PREFIX=${HOME}/.npm-global cd $(dirname $0)/.. npm install -# If tests are running against master, configure Build Cop +# If tests are running against master, configure flakybot # to open issues on failures: if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml export MOCHA_REPORTER=xunit cleanup() { - chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop - $KOKORO_GFILE_DIR/linux_amd64/buildcop + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot } trap cleanup EXIT HUP fi diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh index 606d4321..4d031121 100755 --- a/.kokoro/trampoline_v2.sh +++ b/.kokoro/trampoline_v2.sh @@ -162,7 +162,7 @@ if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then "KOKORO_GITHUB_COMMIT" "KOKORO_GITHUB_PULL_REQUEST_NUMBER" "KOKORO_GITHUB_PULL_REQUEST_COMMIT" - # For Build Cop Bot + # For flakybot "KOKORO_GITHUB_COMMIT_URL" "KOKORO_GITHUB_PULL_REQUEST_URL" ) diff --git a/synth.metadata b/synth.metadata index 14382daa..04d7caca 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "67b0cc3077860a1583bcf18ce50aeff58bbb5496" + "sha": "c27dec9e07d4e6873fdc486d25ed7b50e9560ea6" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "363fe305e9ce34a6cd53951c6ee5f997094b54ee" + "sha": "57c23fa5705499a4181095ced81f0ee0933b64f6" } } ] From 5240fb0e7ba5503d562659a0d1d7c952bc44ce0e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 3 Feb 2021 00:51:43 +0100 Subject: [PATCH 217/662] fix(deps): update dependency puppeteer to v6 (#1129) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a4c5954c..1328ed69 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^5.0.0", + "puppeteer": "^6.0.0", "sinon": "^9.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index d32583e0..45e19345 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^6.0.0", - "puppeteer": "^5.0.0" + "puppeteer": "^6.0.0" } } From 02d0d73a5f0d2fc7de9b13b160e4e7074652f9d0 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 4 Feb 2021 18:29:05 +0100 Subject: [PATCH 218/662] fix(deps): update dependency puppeteer to v7 (#1134) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1328ed69..69b87073 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^6.0.0", + "puppeteer": "^7.0.0", "sinon": "^9.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 45e19345..db1b73ad 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^6.0.0", - "puppeteer": "^6.0.0" + "puppeteer": "^7.0.0" } } From 997f124a5c02dfa44879a759bf701a9fa4c3ba90 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Fri, 5 Feb 2021 17:16:22 -0800 Subject: [PATCH 219/662] feat!: workload identity federation support (#1131) feat: implements the OAuth token exchange spec based on rfc8693 (#1026) feat: defines ExternalAccountClient abstract class for external_account credentials (#1030) feat: adds service account impersonation to `ExternalAccountClient` (#1041) feat: defines `IdentityPoolClient` used for K8s and Azure workloads (#1042) feat: implements AWS signature version 4 for signing requests (#1047) feat: defines `ExternalAccountClient` used to instantiate external account clients (#1050) feat!: integrates external_accounts with `GoogleAuth` and ADC (#1052) feat: adds text/json credential_source support to IdentityPoolClients (#1059) feat: get AWS region from environment variable (#1067) Co-authored-by: Wilfred van der Deijl Co-authored-by: Benjamin E. Coe --- README.md | 212 +- browser-test/test.crypto.ts | 54 +- package.json | 7 +- samples/package.json | 1 + samples/scripts/externalclient-setup.js | 298 +++ samples/test/externalclient.test.js | 392 ++++ src/auth/authclient.ts | 90 +- src/auth/awsclient.ts | 259 +++ src/auth/awsrequestsigner.ts | 305 +++ src/auth/baseexternalclient.ts | 487 +++++ src/auth/externalclient.ts | 80 + src/auth/googleauth.ts | 121 +- src/auth/identitypoolclient.ts | 219 ++ src/auth/oauth2common.ts | 229 ++ src/auth/stscredentials.ts | 228 ++ src/crypto/browser/crypto.ts | 61 +- src/crypto/crypto.ts | 36 + src/crypto/node/crypto.ts | 50 + src/index.ts | 9 + test/externalclienthelper.ts | 141 ++ .../aws-security-credentials-fake.json | 9 + test/fixtures/external-account-cred.json | 9 + test/fixtures/external-subject-token.json | 3 + test/fixtures/external-subject-token.txt | 1 + test/test.awsclient.ts | 712 +++++++ test/test.awsrequestsigner.ts | 742 +++++++ test/test.baseexternalclient.ts | 1847 +++++++++++++++++ test/test.crypto.ts | 81 +- test/test.externalclient.ts | 154 ++ test/test.googleauth.ts | 763 ++++++- test/test.identitypoolclient.ts | 787 +++++++ test/test.index.ts | 3 + test/test.oauth2common.ts | 459 ++++ test/test.stscredentials.ts | 403 ++++ 34 files changed, 9202 insertions(+), 50 deletions(-) create mode 100755 samples/scripts/externalclient-setup.js create mode 100644 samples/test/externalclient.test.js create mode 100644 src/auth/awsclient.ts create mode 100644 src/auth/awsrequestsigner.ts create mode 100644 src/auth/baseexternalclient.ts create mode 100644 src/auth/externalclient.ts create mode 100644 src/auth/identitypoolclient.ts create mode 100644 src/auth/oauth2common.ts create mode 100644 src/auth/stscredentials.ts create mode 100644 test/externalclienthelper.ts create mode 100644 test/fixtures/aws-security-credentials-fake.json create mode 100644 test/fixtures/external-account-cred.json create mode 100644 test/fixtures/external-subject-token.json create mode 100644 test/fixtures/external-subject-token.txt create mode 100644 test/test.awsclient.ts create mode 100644 test/test.awsrequestsigner.ts create mode 100644 test/test.baseexternalclient.ts create mode 100644 test/test.externalclient.ts create mode 100644 test/test.identitypoolclient.ts create mode 100644 test/test.oauth2common.ts create mode 100644 test/test.stscredentials.ts diff --git a/README.md b/README.md index da54ef90..375f7bbc 100644 --- a/README.md +++ b/README.md @@ -48,16 +48,19 @@ npm install google-auth-library ## Ways to authenticate This library provides a variety of ways to authenticate to your Google services. -- [Application Default Credentials](#choosing-the-correct-credential-type-automatically) - Use Application Default Credentials when you use a single identity for all users in your application. Especially useful for applications running on Google Cloud. +- [Application Default Credentials](#choosing-the-correct-credential-type-automatically) - Use Application Default Credentials when you use a single identity for all users in your application. Especially useful for applications running on Google Cloud. Application Default Credentials also support workload identity federation to access Google Cloud resources from non-Google Cloud platforms. - [OAuth 2](#oauth2) - Use OAuth2 when you need to perform actions on behalf of the end user. - [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication. - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. +- [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). ## Application Default Credentials This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. +Application Default Credentials also support workload identity federation to access Google Cloud resources from non-Google Cloud platforms including Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). Workload identity federation is recommended for non-Google Cloud environments as it avoids the need to download, manage and store service account private keys locally, see: [Workload Identity Federation](#workload-identity-federation). + #### Download your Service Account Credentials JSON file To use Application Default Credentials, You first need to download a set of JSON credentials for your project. Go to **APIs & Auth** > **Credentials** in the [Google Developers Console](https://console.cloud.google.com/) and select **Service account** from the **Add credentials** dropdown. @@ -363,6 +366,213 @@ async function main() { main().catch(console.error); ``` +## Workload Identity Federation + +Using workload identity federation, your application can access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). + +Traditionally, applications running outside Google Cloud have used service account keys to access Google Cloud resources. Using identity federation, you can allow your workload to impersonate a service account. +This lets you access Google Cloud resources directly, eliminating the maintenance and security burden associated with service account keys. + +### Accessing resources from AWS + +In order to access Google Cloud resources from Amazon Web Services (AWS), the following requirements are needed: +- A workload identity pool needs to be created. +- AWS needs to be added as an identity provider in the workload identity pool (The Google [organization policy](https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict) needs to allow federation from AWS). +- Permission to impersonate a service account needs to be granted to the external identity. + +Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-resources-aws) on how to configure workload identity federation from AWS. + +After configuring the AWS provider to impersonate a service account, a credential configuration file needs to be generated. +Unlike service account credential files, the generated credential configuration file will only contain non-sensitive metadata to instruct the library on how to retrieve external subject tokens and exchange them for service account access tokens. +The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). + +To generate the AWS workload identity configuration, run the following command: + +```bash +# Generate an AWS configuration file. +gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AWS_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --aws \ + --output-file /path/to/generated/config.json +``` + +Where the following variables need to be substituted: +- `$PROJECT_NUMBER`: The Google Cloud project number. +- `$POOL_ID`: The workload identity pool ID. +- `$AWS_PROVIDER_ID`: The AWS provider ID. +- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + +This will generate the configuration file in the specified output file. + +You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from AWS. + +### Access resources from Microsoft Azure + +In order to access Google Cloud resources from Microsoft Azure, the following requirements are needed: +- A workload identity pool needs to be created. +- Azure needs to be added as an identity provider in the workload identity pool (The Google [organization policy](https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict) needs to allow federation from Azure). +- The Azure tenant needs to be configured for identity federation. +- Permission to impersonate a service account needs to be granted to the external identity. + +Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-resources-azure) on how to configure workload identity federation from Microsoft Azure. + +After configuring the Azure provider to impersonate a service account, a credential configuration file needs to be generated. +Unlike service account credential files, the generated credential configuration file will only contain non-sensitive metadata to instruct the library on how to retrieve external subject tokens and exchange them for service account access tokens. +The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). + +To generate the Azure workload identity configuration, run the following command: + +```bash +# Generate an Azure configuration file. +gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AZURE_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --azure \ + --output-file /path/to/generated/config.json +``` + +Where the following variables need to be substituted: +- `$PROJECT_NUMBER`: The Google Cloud project number. +- `$POOL_ID`: The workload identity pool ID. +- `$AZURE_PROVIDER_ID`: The Azure provider ID. +- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + +This will generate the configuration file in the specified output file. + +You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from Azure. + +### Accessing resources from an OIDC identity provider + +In order to access Google Cloud resources from an identity provider that supports [OpenID Connect (OIDC)](https://openid.net/connect/), the following requirements are needed: +- A workload identity pool needs to be created. +- An OIDC identity provider needs to be added in the workload identity pool (The Google [organization policy](https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict) needs to allow federation from the identity provider). +- Permission to impersonate a service account needs to be granted to the external identity. + +Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-resources-oidc) on how to configure workload identity federation from an OIDC identity provider. + +After configuring the OIDC provider to impersonate a service account, a credential configuration file needs to be generated. +Unlike service account credential files, the generated credential configuration file will only contain non-sensitive metadata to instruct the library on how to retrieve external subject tokens and exchange them for service account access tokens. +The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). + +For OIDC providers, the Auth library can retrieve OIDC tokens either from a local file location (file-sourced credentials) or from a local server (URL-sourced credentials). + +**File-sourced credentials** +For file-sourced credentials, a background process needs to be continuously refreshing the file location with a new OIDC token prior to expiration. +For tokens with one hour lifetimes, the token needs to be updated in the file every hour. The token can be stored directly as plain text or in JSON format. + +To generate a file-sourced OIDC configuration, run the following command: + +```bash +# Generate an OIDC configuration file for file-sourced credentials. +gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$OIDC_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --credential-source-file $PATH_TO_OIDC_ID_TOKEN \ + # Optional arguments for file types. Default is "text": + # --credential-source-type "json" \ + # Optional argument for the field that contains the OIDC credential. + # This is required for json. + # --credential-source-field-name "id_token" \ + --output-file /path/to/generated/config.json +``` + +Where the following variables need to be substituted: +- `$PROJECT_NUMBER`: The Google Cloud project number. +- `$POOL_ID`: The workload identity pool ID. +- `$OIDC_PROVIDER_ID`: The OIDC provider ID. +- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. +- `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from. + +This will generate the configuration file in the specified output file. + +**URL-sourced credentials** +For URL-sourced credentials, a local server needs to host a GET endpoint to return the OIDC token. The response can be in plain text or JSON. +Additional required request headers can also be specified. + +To generate a URL-sourced OIDC workload identity configuration, run the following command: + +```bash +# Generate an OIDC configuration file for URL-sourced credentials. +gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$OIDC_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --credential-source-url $URL_TO_GET_OIDC_TOKEN \ + --credential-source-headers $HEADER_KEY=$HEADER_VALUE \ + # Optional arguments for file types. Default is "text": + # --credential-source-type "json" \ + # Optional argument for the field that contains the OIDC credential. + # This is required for json. + # --credential-source-field-name "id_token" \ + --output-file /path/to/generated/config.json +``` + +Where the following variables need to be substituted: +- `$PROJECT_NUMBER`: The Google Cloud project number. +- `$POOL_ID`: The workload identity pool ID. +- `$OIDC_PROVIDER_ID`: The OIDC provider ID. +- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. +- `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. +- `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. + +You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC provider. + +### Using External Identities + +External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. +In order to use external identities with Application Default Credentials, you need to generate the JSON credentials configuration file for your external identity as described above. +Once generated, store the path to this file in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. + +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/config.json +``` + +The library can now automatically choose the right type of client and initialize credentials from the context provided in the configuration file. + +```js +async function main() { + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform' + }); + const client = await auth.getClient(); + const projectId = await auth.getProjectId(); + // List all buckets in a project. + const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; + const res = await client.request({ url }); + console.log(res.data); +} +``` + +When using external identities with Application Default Credentials in Node.js, the `roles/browser` role needs to be granted to the service account. +The `Cloud Resource Manager API` should also be enabled on the project. +This is needed since the library will try to auto-discover the project ID from the current environment using the impersonated credential. +To avoid this requirement, the project ID can be explicitly specified on initialization. + +```js +const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + // Pass the project ID explicitly to avoid the need to grant `roles/browser` to the service account + // or enable Cloud Resource Manager API on the project. + projectId: 'CLOUD_RESOURCE_PROJECT_ID', +}); +``` + +You can also explicitly initialize external account clients using the generated configuration file. + +```js +const {ExternalAccountClient} = require('google-auth-library'); +const jsonConfig = require('/path/to/config.json'); + +async function main() { + const client = ExternalAccountClient.fromJSON(jsonConfig); + client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; + // List all buckets in a project. + const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; + const res = await client.request({url}); + console.log(res.data); +} +``` + ## Working with ID Tokens ### Fetching ID Tokens If your application is running on Cloud Run or Cloud Functions, or using Cloud Identity-Aware diff --git a/browser-test/test.crypto.ts b/browser-test/test.crypto.ts index aaa13b3f..5da17f98 100644 --- a/browser-test/test.crypto.ts +++ b/browser-test/test.crypto.ts @@ -14,7 +14,7 @@ import * as base64js from 'base64-js'; import {assert} from 'chai'; -import {createCrypto} from '../src/crypto/crypto'; +import {createCrypto, fromArrayBufferToHex} from '../src/crypto/crypto'; import {BrowserCrypto} from '../src/crypto/browser/crypto'; import {privateKey, publicKey} from './fixtures/keys'; import {describe, it} from 'mocha'; @@ -99,4 +99,56 @@ describe('Browser crypto tests', () => { const encodedString = crypto.encodeBase64StringUtf8(originalString); assert.strictEqual(encodedString, base64String); }); + + it('should calculate SHA256 digest in hex encoding', async () => { + const input = 'I can calculate SHA256'; + const expectedHexDigest = + '73d08486d8bfd4fb4bc12dd8903604ddbde5ad95b6efa567bd723ce81a881122'; + + const calculatedHexDigest = await crypto.sha256DigestHex(input); + assert.strictEqual(calculatedHexDigest, expectedHexDigest); + }); + + describe('should compute the HMAC-SHA256 hash of a message', () => { + it('using a string key', async () => { + const message = 'The quick brown fox jumps over the lazy dog'; + const key = 'key'; + const expectedHexHash = + 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'; + const expectedHash = new Uint8Array( + (expectedHexHash.match(/.{1,2}/g) as string[]).map(byte => + parseInt(byte, 16) + ) + ); + + const calculatedHash = await crypto.signWithHmacSha256(key, message); + assert.deepStrictEqual(calculatedHash, expectedHash.buffer); + }); + + it('using an ArrayBuffer key', async () => { + const message = 'The quick brown fox jumps over the lazy dog'; + // String "key" ArrayBuffer representation. + const key = new Uint8Array([107, 0, 101, 0, 121, 0]) + .buffer as ArrayBuffer; + const expectedHexHash = + 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'; + const expectedHash = new Uint8Array( + (expectedHexHash.match(/.{1,2}/g) as string[]).map(byte => + parseInt(byte, 16) + ) + ); + + const calculatedHash = await crypto.signWithHmacSha256(key, message); + assert.deepStrictEqual(calculatedHash, expectedHash.buffer); + }); + }); + + it('should expose a method to convert an ArrayBuffer to hex', () => { + const arrayBuffer = new Uint8Array([4, 8, 0, 12, 16, 0]) + .buffer as ArrayBuffer; + const expectedHexEncoding = '0408000c1000'; + + const calculatedHexEncoding = fromArrayBufferToHex(arrayBuffer); + assert.strictEqual(calculatedHexEncoding, expectedHexEncoding); + }); }); diff --git a/package.json b/package.json index 69b87073..c4737d1e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ }, "devDependencies": { "@compodoc/compodoc": "^1.1.7", + "@microsoft/api-documenter": "^7.8.10", + "@microsoft/api-extractor": "^7.8.10", "@types/base64-js": "^1.2.5", "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", @@ -67,9 +69,7 @@ "ts-loader": "^8.0.0", "typescript": "^3.8.3", "webpack": "^4.20.2", - "webpack-cli": "^4.0.0", - "@microsoft/api-documenter": "^7.8.10", - "@microsoft/api-extractor": "^7.8.10" + "webpack-cli": "^4.0.0" }, "files": [ "build/src", @@ -84,6 +84,7 @@ "fix": "gts fix", "pretest": "npm run compile", "docs": "compodoc src/", + "samples-setup": "cd samples/ && npm link ../ && npm run setup && cd ../", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "system-test": "mocha build/system-test --timeout 60000", "presystem-test": "npm run compile", diff --git a/samples/package.json b/samples/package.json index 75269a2a..b9b5064d 100644 --- a/samples/package.json +++ b/samples/package.json @@ -5,6 +5,7 @@ "*.js" ], "scripts": { + "setup": "node scripts/externalclient-setup.js", "test": "mocha --timeout 60000" }, "engines": { diff --git a/samples/scripts/externalclient-setup.js b/samples/scripts/externalclient-setup.js new file mode 100755 index 00000000..e8a5ce4b --- /dev/null +++ b/samples/scripts/externalclient-setup.js @@ -0,0 +1,298 @@ +// Copyright 2021 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. + +// This script is used to generate the project configurations needed to +// end-to-end test workload identity pools in the Auth library, specifically +// file-sourced, URL-sourced OIDC-based credentials and AWS credentials. +// This is done via the sample test: samples/test/externalclient.test.js. +// +// In order to run this script, the GOOGLE_APPLICATION_CREDENTIALS environment +// variable needs to be set to point to a service account key file. +// Additional AWS related information (AWS account ID and AWS role name) also +// need to be provided in this file. Detailed instructions are documented below. +// +// GCP project changes: +// -------------------- +// The following IAM roles need to be set on the service account: +// 1. IAM Workload Identity Pool Admin (needed to create resources for workload +// identity pools). +// 2. Security Admin (needed to get and set IAM policies). +// 3. Service Account Token Creator (needed to generate Google ID tokens and +// access tokens). +// +// The following APIs need to be enabled on the project: +// 1. Identity and Access Management (IAM) API. +// 2. IAM Service Account Credentials API. +// 3. Cloud Resource Manager API. +// 4. The API being accessed in the test, eg. DNS. +// +// AWS developer account changes: +// ------------------------------ +// For testing AWS credentials, the following are needed: +// 1. An AWS developer account is needed. The account ID will need to +// be provided in the configuration object below. +// 2. A role for web identity federation. This will also need to be provided +// in the configuration object below. +// - An OIDC Google identity provider needs to be created with the following: +// issuer: accounts.google.com +// audience: Use the client_id of the service account. +// - A role for OIDC web identity federation is needed with the created +// Google provider as a trusted entity: +// "accounts.google.com:aud": "$CLIENT_ID" +// The role creation steps are documented at: +// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html +// +// This script needs to be run once. It will do the following: +// 1. Create a random workload identity pool. +// 2. Create a random OIDC provider in that pool which uses the +// accounts.google.com as the issuer and the default STS audience as the +// allowed audience. This audience will be validated on STS token exchange. +// 3. Enable OIDC tokens generated by the current service account to impersonate +// the service account. (Identified by the OIDC token sub field which is the +// service account client ID). +// 4. Create a random AWS provider in that pool which uses the provided AWS +// account ID. +// 5. Enable AWS provider to impersonate the service account. (Principal is +// identified by the AWS role name). +// 6. Print out the STS audience fields associated with the created providers +// after the setup completes successfully so that they can be used in the +// tests. These will be copied and used as the global AUDIENCE_OIDC and +// AUDIENCE_AWS constants in samples/test/externalclient.test.js. +// An additional AWS_ROLE_ARN field will be printed out and also needs +// to be copied to the test file. This will be used as the AWS role for +// AssumeRoleWithWebIdentity when federating from GCP to AWS. +// The same service account used for this setup script should be used for +// the test script. +// +// It is safe to run the setup script again. A new pool is created and new +// audiences are printed. If run multiple times, it is advisable to delete +// unused pools. Note that deleted pools are soft deleted and may remain for +// a while before they are completely deleted. The old pool ID cannot be used +// in the meantime. + +const fs = require('fs'); +const {promisify} = require('util'); +const {GoogleAuth} = require('google-auth-library'); + +const readFile = promisify(fs.readFile); + +/** + * Generates a random string of the specified length, optionally using the + * specified alphabet. + * + * @param {number} length The length of the string to generate. + * @return {string} A random string of the provided length. + */ +function generateRandomString(length) { + const chars = []; + const allowedChars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < length; i++) { + chars.push( + allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)) + ); + } + return chars.join(''); +} + +/** + * Creates a workload identity pool with an OIDC provider which will accept + * Google OIDC tokens generated from the current service account where the token + * will have sub as the service account client ID and the audience as the + * created identity pool STS audience. + * The steps followed here mirror the instructions for configuring federation + * with an OIDC provider illustrated at: + * https://cloud.google.com/iam/docs/access-resources-oidc + * This will also create an AWS provider in the same workload identity pool + * using the AWS account ID and AWS ARN role name provided. + * The steps followed here mirror the instructions for configuring federation + * with an AWS provider illustrated at: + * https://cloud.google.com/iam/docs/access-resources-aws + * @param {Object} config An object containing additional data needed to + * configure the external account client setup. + * @return {Promise} A promise that resolves with an object containing + * the STS audience corresponding with the generated workload identity pool + * OIDC provider and AWS provider. + */ +async function main(config) { + let response; + const keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS; + if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) { + throw new Error('No GOOGLE_APPLICATION_CREDENTIALS env var is available'); + } + const keys = JSON.parse(await readFile(keyFile, 'utf8')); + const suffix = generateRandomString(10); + const poolId = `pool-${suffix}`; + const oidcProviderId = `oidc-${suffix}`; + const awsProviderId = `aws-${suffix}`; + const projectId = keys.project_id; + const clientEmail = keys.client_email; + const sub = keys.client_id; + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }); + + // TODO: switch to using IAM client SDK once v1 API has all the v1beta + // changes. + // https://cloud.google.com/iam/docs/reference/rest/v1beta/projects.locations.workloadIdentityPools + // https://github.com/googleapis/google-api-nodejs-client/tree/master/src/apis/iam + + // Create the workload identity pool. + response = await auth.request({ + url: + `https://iam.googleapis.com/v1beta/projects/${projectId}/` + + `locations/global/workloadIdentityPools?workloadIdentityPoolId=${poolId}`, + method: 'POST', + data: { + displayName: 'Test workload identity pool', + description: 'Test workload identity pool for Node.js', + }, + }); + // Populate the audience field. This will be used by the tests, specifically + // the credential configuration file. + const poolResourcePath = response.data.name.split('/operations')[0]; + const oidcAudience = `//iam.googleapis.com/${poolResourcePath}/providers/${oidcProviderId}`; + const awsAudience = `//iam.googleapis.com/${poolResourcePath}/providers/${awsProviderId}`; + + // Allow service account impersonation. + // Get the existing IAM policity bindings on the current service account. + response = await auth.request({ + url: + `https://iam.googleapis.com/v1/projects/${projectId}/` + + `serviceAccounts/${clientEmail}:getIamPolicy`, + method: 'POST', + }); + const bindings = response.data.bindings || []; + // If not found, add roles/iam.workloadIdentityUser role binding to the + // workload identity pool member. + // For OIDC providers, we will use the value mapped to google.subject. + // This is the sub field of the OIDC token which is the service account + // client_id. + // For AWS providers, we will use the AWS role attribute. This will be the + // assumed role by AssumeRoleWithWebIdentity. + let found = false; + bindings.forEach(binding => { + if (binding.role === 'roles/iam.workloadIdentityUser') { + found = true; + binding.members = [ + `principal://iam.googleapis.com/${poolResourcePath}/subject/${sub}`, + `principalSet://iam.googleapis.com/${poolResourcePath}/` + + `attribute.aws_role/arn:aws:sts::${config.awsAccountId}:assumed-role/` + + `${config.awsRoleName}`, + ]; + } + }); + if (!found) { + bindings.push({ + role: 'roles/iam.workloadIdentityUser', + members: [ + `principal://iam.googleapis.com/${poolResourcePath}/subject/${sub}`, + `principalSet://iam.googleapis.com/${poolResourcePath}/` + + `attribute.aws_role/arn:aws:sts::${config.awsAccountId}:assumed-role/` + + `${config.awsRoleName}`, + ], + }); + } + await auth.request({ + url: + `https://iam.googleapis.com/v1/projects/${projectId}/` + + `serviceAccounts/${clientEmail}:setIamPolicy`, + method: 'POST', + data: { + policy: { + bindings, + }, + }, + }); + + // Create an OIDC provider. This will use the accounts.google.com issuer URL. + // This will use the STS audience as the OIDC token audience. + await auth.request({ + url: + `https://iam.googleapis.com/v1beta/projects/${projectId}/` + + `locations/global/workloadIdentityPools/${poolId}/providers?` + + `workloadIdentityPoolProviderId=${oidcProviderId}`, + method: 'POST', + data: { + displayName: 'Test OIDC provider', + description: 'Test OIDC provider for Node.js', + attributeMapping: { + 'google.subject': 'assertion.sub', + }, + oidc: { + issuerUri: 'https://accounts.google.com', + allowedAudiences: [oidcAudience], + }, + }, + }); + + // Create an AWS provider. + await auth.request({ + url: + `https://iam.googleapis.com/v1beta/projects/${projectId}/` + + `locations/global/workloadIdentityPools/${poolId}/providers?` + + `workloadIdentityPoolProviderId=${awsProviderId}`, + method: 'POST', + data: { + displayName: 'Test AWS provider', + description: 'Test AWS provider for Node.js', + aws: { + accountId: config.awsAccountId, + }, + }, + }); + + return { + oidcAudience, + awsAudience, + }; +} + +// Additional configuration input needed to configure the workload +// identity pool. For AWS tests, an AWS developer account is needed. +// The following AWS prerequisite setup is needed. +// 1. An OIDC Google identity provider needs to be created with the following: +// issuer: accounts.google.com +// audience: Use the client_id of the service account. +// 2. A role for OIDC web identity federation is needed with the created Google +// provider as a trusted entity: +// "accounts.google.com:aud": "$CLIENT_ID" +// The steps are documented at: +// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html +const config = { + // The role name for web identity federation. + awsRoleName: 'ci-nodejs-test', + // The AWS account ID. + awsAccountId: '077071391996', +}; + +// On execution, the following will be printed to the screen: +// AUDIENCE_OIDC: generated OIDC provider audience. +// AUDIENCE_AWS: generated AWS provider audience. +// AWS_ROLE_ARN: This is the AWS role for AssumeRoleWithWebIdentity. +// This should be updated in test/externalclient.test.js. +// Some delay is needed before running the tests in test/externalclient.test.js +// to ensure IAM policies propagate before running sample tests. +// Normally 1-2 minutes should suffice. +main(config) + .then(audiences => { + console.log( + 'The following constants need to be set in test/externalclient.test.js' + ); + console.log(`AUDIENCE_OIDC='${audiences.oidcAudience}'`); + console.log(`AUDIENCE_AWS='${audiences.awsAudience}'`); + console.log( + `AWS_ROLE_ARN='arn:aws::iam::${config.awsAccountId}:role/${config.awsRoleName}'` + ); + }) + .catch(console.error); diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js new file mode 100644 index 00000000..05f699ef --- /dev/null +++ b/samples/test/externalclient.test.js @@ -0,0 +1,392 @@ +// Copyright 2021 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. + +// Prerequisites: +// Make sure to run the setup in samples/scripts/externalclient-setup.js +// and copy the logged constant strings (AUDIENCE_OIDC, AUDIENCE_AWS and +// AWS_ROLE_ARN) into this file before running this test suite. +// Once that is done, this test can be run indefinitely. +// +// The only requirement for this test suite to run is to set the environment +// variable GOOGLE_APPLICATION_CREDENTIALS to point to the same service account +// keys used in the setup script. +// +// This script follows the following logic. +// 1. OIDC provider (file-sourced and url-sourced credentials): +// Use the service account keys to generate a Google ID token using the +// iamcredentials generateIdToken API, using the default STS audience. +// This will use the service account client ID as the sub field of the token. +// This OIDC token will be used as the external subject token to be exchanged +// for a Google access token via GCP STS endpoint and then to impersonate the +// original service account key. This is abstracted by the GoogleAuth library. +// 2. AWS provider: +// Use the service account keys to generate a Google ID token using the +// iamcredentials generateIdToken API, using the client_id as audience. +// Exchange the OIDC ID token for AWS security keys using AWS STS +// AssumeRoleWithWebIdentity API. These values will be set as AWS environment +// variables to simulate an AWS VM. The Auth library can now read these +// variables and create a signed request to AWS GetCallerIdentity. This will +// be used as the external subject token to be exchanged for a Google access +// token via GCP STS endpoint and then to impersonate the original service +// account key. This is abstracted by the GoogleAuth library. +// +// OIDC provider tests for file-sourced and url-sourced credentials +// ---------------------------------------------------------------- +// The test suite will run tests for file-sourced and url-sourced credentials. +// In both cases, the same Google OIDC token is used as the underlying subject +// token. +// +// AWS provider tests for AWS credentials +// ------------------------------------- +// The test suite will also run tests for AWS credentials. This works as +// follows. (Note prequisite setup is needed. This is documented in +// externalclient-setup.js). +// - iamcredentials:generateIdToken is used to generate a Google ID token using +// the service account access token. The service account client_id is used as +// audience. +// - AWS STS AssumeRoleWithWebIdentity API is used to exchange this token for +// temporary AWS security credentials for a specified AWS ARN role. +// - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN +// environment variables are set using these credentials before the test is +// run simulating an AWS VM. +// - The test can now be run. +// +// For each test, a sample script is run in a child process with +// GOOGLE_APPLICATION_CREDENTIALS environment variable pointing to a temporary +// workload identity pool credentials configuration. A Cloud API is called in +// the process and the expected output is confirmed. + +const cp = require('child_process'); +const {assert} = require('chai'); +const {describe, it, before, afterEach} = require('mocha'); +const fs = require('fs'); +const {promisify} = require('util'); +const {GoogleAuth, DefaultTransporter} = require('google-auth-library'); +const os = require('os'); +const path = require('path'); +const http = require('http'); + +/** + * Runs the provided command using asynchronous child_process.exec. + * Unlike execSync, this works with another local HTTP server running in the + * background. + * @param {string} cmd The actual command string to run. + * @param {*} opts The optional parameters for child_process.exec. + * @return {Promise} A promise that resolves with a string + * corresponding with the terminal output. + */ +const execAsync = async (cmd, opts) => { + const {stdout, stderr} = await exec(cmd, opts); + return stdout + stderr; +}; + +/** + * Generates a Google ID token using the iamcredentials generateIdToken API. + * https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oidc + * + * @param {GoogleAuth} auth The GoogleAuth instance. + * @param {string} aud The Google ID token audience. + * @param {string} clientEmail The service account client email. + * @return {Promise} A promise that resolves with the generated Google + * ID token. + */ +const generateGoogleIdToken = async (auth, aud, clientEmail) => { + // roles/iam.serviceAccountTokenCreator role needed. + const response = await auth.request({ + url: + 'https://iamcredentials.googleapis.com/v1/' + + `projects/-/serviceAccounts/${clientEmail}:generateIdToken`, + method: 'POST', + data: { + audience: aud, + includeEmail: true, + }, + }); + return response.data.token; +}; + +/** + * Rudimentary value lookup within an XML file by tagName. + * @param {string} rawXml The raw XML string. + * @param {string} tagName The name of the tag whose value is to be returned. + * @return {?string} The value if found, null otherwise. + */ +const getXmlValueByTagName = (rawXml, tagName) => { + const startIndex = rawXml.indexOf(`<${tagName}>`); + const endIndex = rawXml.indexOf(``, startIndex); + if (startIndex >= 0 && endIndex > startIndex) { + return rawXml.substring(startIndex + tagName.length + 2, endIndex); + } + return null; +}; + +/** + * Generates a Google OIDC ID token and exchanges it for AWS security credentials + * using the AWS STS AssumeRoleWithWebIdentity API. + * @param {GoogleAuth} auth The GoogleAuth instance. + * @param {string} aud The Google ID token audience. + * @param {string} clientEmail The service account client email. + * @param {string} awsRoleArn The Amazon Resource Name (ARN) of the role that + * the caller is assuming. + * @return {Promise} A promise that resolves with the generated AWS + * security credentials. + */ +const assumeRoleWithWebIdentity = async ( + auth, + aud, + clientEmail, + awsRoleArn +) => { + // API documented at: + // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html + // Note that a role for web identity or OIDC federation will need to have + // been configured: + // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html + const oidcToken = await generateGoogleIdToken(auth, aud, clientEmail); + const transporter = new DefaultTransporter(); + const url = + 'https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity' + + '&Version=2011-06-15&DurationSeconds=3600&RoleSessionName=nodejs-test' + + `&RoleArn=${awsRoleArn}&WebIdentityToken=${oidcToken}`; + // The response is in XML format but we will parse it as text. + const response = await transporter.request({url, responseType: 'text'}); + const rawXml = response.data; + return { + awsAccessKeyId: getXmlValueByTagName(rawXml, 'AccessKeyId'), + awsSecretAccessKey: getXmlValueByTagName(rawXml, 'SecretAccessKey'), + awsSessionToken: getXmlValueByTagName(rawXml, 'SessionToken'), + }; +}; + +/** + * Generates a random string of the specified length, optionally using the + * specified alphabet. + * + * @param {number} length The length of the string to generate. + * @return {string} A random string of the provided length. + */ +const generateRandomString = length => { + const chars = []; + const allowedChars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + while (length > 0) { + chars.push( + allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)) + ); + length--; + } + return chars.join(''); +}; + +////////////////////////////////////////////////////////////////////////// +// Copy values from the output of samples/scripts/externalclient-setup.js. +// OIDC provider STS audience. +const AUDIENCE_OIDC = + '//iam.googleapis.com/projects/1046198160504/locations/global/' + + 'workloadIdentityPools/pool-95vux39vzm/providers/oidc-95vux39vzm'; +// AWS provider STS audience. +const AUDIENCE_AWS = + '//iam.googleapis.com/projects/1046198160504/locations/global/' + + 'workloadIdentityPools/pool-95vux39vzm/providers/aws-95vux39vzm'; +// AWS ARN role used for federating from GCP to AWS via +// AssumeRoleWithWebIdentity. +const AWS_ROLE_ARN = 'arn:aws:iam::077071391996:role/ci-nodejs-test'; +////////////////////////////////////////////////////////////////////////// +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); +const unlink = promisify(fs.unlink); +const exec = promisify(cp.exec); +const keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS; + +describe('samples for external-account', () => { + let httpServer; + let clientEmail; + let oidcToken; + let awsCredentials; + const port = 8088; + const suffix = generateRandomString(10); + const configFilePath = path.join(os.tmpdir(), `config-${suffix}.json`); + const oidcTokenFilePath = path.join(os.tmpdir(), `token-${suffix}.txt`); + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }); + + before(async () => { + const keys = JSON.parse(await readFile(keyFile, 'utf8')); + const clientId = keys.client_id; + clientEmail = keys.client_email; + + // Generate the Google OIDC token. This will be used as the external + // subject token for the following OIDC file-sourced and url-sourced + // credential tests. + oidcToken = await generateGoogleIdToken(auth, AUDIENCE_OIDC, clientEmail); + // Generate the AWS security keys. These will be used to similate an + // AWS VM to test external account AWS credentials. + awsCredentials = await assumeRoleWithWebIdentity( + auth, + clientId, + clientEmail, + AWS_ROLE_ARN + ); + }); + + afterEach(async () => { + // Delete temporary files. + if (fs.existsSync(configFilePath)) { + await unlink(configFilePath); + } + if (fs.existsSync(oidcTokenFilePath)) { + await unlink(oidcTokenFilePath); + } + // Close any open http servers. + if (httpServer) { + httpServer.close(); + } + }); + + it('should acquire ADC for file-sourced creds', async () => { + // Create file-sourced configuration JSON file. + // The created OIDC token will be used as the subject token and will be + // retrieved from a file location. + const config = { + type: 'external_account', + audience: AUDIENCE_OIDC, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: 'https://sts.googleapis.com/v1beta/token', + service_account_impersonation_url: + 'https://iamcredentials.googleapis.com/v1/projects/' + + `-/serviceAccounts/${clientEmail}:generateAccessToken`, + credential_source: { + file: oidcTokenFilePath, + }, + }; + await writeFile(oidcTokenFilePath, oidcToken); + await writeFile(configFilePath, JSON.stringify(config)); + + // Run sample script with GOOGLE_APPLICATION_CREDENTIALS envvar + // pointing to the temporarily created configuration file. + const output = await execAsync(`${process.execPath} adc`, { + env: { + ...process.env, + GOOGLE_APPLICATION_CREDENTIALS: configFilePath, + }, + }); + // Confirm expected script output. + assert.match(output, /DNS Info:/); + }); + + it('should acquire ADC for url-sourced creds', async () => { + // Create url-sourced configuration JSON file. + // The created OIDC token will be used as the subject token and will be + // retrieved from a local server. + const config = { + type: 'external_account', + audience: AUDIENCE_OIDC, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: 'https://sts.googleapis.com/v1beta/token', + service_account_impersonation_url: + 'https://iamcredentials.googleapis.com/v1/projects/' + + `-/serviceAccounts/${clientEmail}:generateAccessToken`, + credential_source: { + url: `http://localhost:${port}/token`, + headers: { + 'my-header': 'some-value', + }, + format: { + type: 'json', + subject_token_field_name: 'access_token', + }, + }, + }; + await writeFile(configFilePath, JSON.stringify(config)); + // Start local metadata server. This will expose a /token + // endpoint to return the OIDC token in JSON format. + httpServer = http.createServer((req, res) => { + if (req.url === '/token' && req.method === 'GET') { + // Confirm expected header is passed along the request. + if (req.headers['my-header'] === 'some-value') { + res.setHeader('Content-Type', 'application/json'); + res.writeHead(200); + res.end( + JSON.stringify({ + access_token: oidcToken, + }) + ); + } else { + res.setHeader('Content-Type', 'application/json'); + res.writeHead(400); + res.end( + JSON.stringify({ + error: 'missing-header', + }) + ); + } + } else { + res.writeHead(404); + res.end(JSON.stringify({error: 'Resource not found'})); + } + }); + await new Promise(resolve => { + httpServer.listen(port, resolve); + }); + + // Run sample script with GOOGLE_APPLICATION_CREDENTIALS environment + // variable pointing to the temporarily created configuration file. + const output = await execAsync(`${process.execPath} adc`, { + env: { + ...process.env, + GOOGLE_APPLICATION_CREDENTIALS: configFilePath, + }, + }); + // Confirm expected script output. + assert.match(output, /DNS Info:/); + }); + + it('should acquire ADC for AWS creds', async () => { + // Create AWS configuration JSON file. + const config = { + type: 'external_account', + audience: AUDIENCE_AWS, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: 'https://sts.googleapis.com/v1beta/token', + service_account_impersonation_url: + 'https://iamcredentials.googleapis.com/v1/projects/' + + `-/serviceAccounts/${clientEmail}:generateAccessToken`, + credential_source: { + environment_id: 'aws1', + regional_cred_verification_url: + 'https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15', + }, + }; + await writeFile(configFilePath, JSON.stringify(config)); + + // Run sample script with GOOGLE_APPLICATION_CREDENTIALS environment + // variable pointing to the temporarily created configuration file. + // Populate AWS environment variables to simulate an AWS VM. + const output = await execAsync(`${process.execPath} adc`, { + env: { + ...process.env, + // AWS environment variables: hardcoded region + AWS security + // credentials. + AWS_REGION: 'us-east-2', + AWS_ACCESS_KEY_ID: awsCredentials.awsAccessKeyId, + AWS_SECRET_ACCESS_KEY: awsCredentials.awsSecretAccessKey, + AWS_SESSION_TOKEN: awsCredentials.awsSessionToken, + // GOOGLE_APPLICATION_CREDENTIALS environment variable used for ADC. + GOOGLE_APPLICATION_CREDENTIALS: configFilePath, + }, + }); + // Confirm expected script output. + assert.match(output, /DNS Info:/); + }); +}); diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index f7d4d13c..bc1dea8b 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -13,26 +13,112 @@ // limitations under the License. import {EventEmitter} from 'events'; -import {GaxiosOptions, GaxiosPromise} from 'gaxios'; +import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; import {DefaultTransporter} from '../transporters'; import {Credentials} from './credentials'; import {Headers} from './oauth2client'; +/** + * Defines the root interface for all clients that generate credentials + * for calling Google APIs. All clients should implement this interface. + */ +export interface CredentialsClient { + /** + * The project ID corresponding to the current credentials if available. + */ + projectId?: string | null; + + /** + * The expiration threshold in milliseconds before forcing token refresh. + */ + eagerRefreshThresholdMillis: number; + + /** + * Whether to force refresh on failure when making an authorization request. + */ + forceRefreshOnFailure: boolean; + + /** + * @return A promise that resolves with the current GCP access token + * response. If the current credential is expired, a new one is retrieved. + */ + getAccessToken(): Promise<{ + token?: string | null; + res?: GaxiosResponse | null; + }>; + + /** + * The main authentication interface. It takes an optional url which when + * present is the endpoint being accessed, and returns a Promise which + * resolves with authorization header fields. + * + * The result has the form: + * { Authorization: 'Bearer ' } + * @param url The URI being authorized. + */ + getRequestHeaders(url?: string): Promise; + + /** + * Provides an alternative Gaxios request implementation with auth credentials + */ + request(opts: GaxiosOptions): GaxiosPromise; + + /** + * Sets the auth credentials. + */ + setCredentials(credentials: Credentials): void; + + /** + * Subscribes a listener to the tokens event triggered when a token is + * generated. + * + * @param event The tokens event to subscribe to. + * @param listener The listener that triggers on event trigger. + * @return The current client instance. + */ + on(event: 'tokens', listener: (tokens: Credentials) => void): this; +} + export declare interface AuthClient { on(event: 'tokens', listener: (tokens: Credentials) => void): this; } -export abstract class AuthClient extends EventEmitter { +export abstract class AuthClient + extends EventEmitter + implements CredentialsClient { protected quotaProjectId?: string; transporter = new DefaultTransporter(); credentials: Credentials = {}; + projectId?: string | null; + eagerRefreshThresholdMillis = 5 * 60 * 1000; + forceRefreshOnFailure = false; /** * Provides an alternative Gaxios request implementation with auth credentials */ abstract request(opts: GaxiosOptions): GaxiosPromise; + /** + * The main authentication interface. It takes an optional url which when + * present is the endpoint being accessed, and returns a Promise which + * resolves with authorization header fields. + * + * The result has the form: + * { Authorization: 'Bearer ' } + * @param url The URI being authorized. + */ + abstract getRequestHeaders(url?: string): Promise; + + /** + * @return A promise that resolves with the current GCP access token + * response. If the current credential is expired, a new one is retrieved. + */ + abstract getAccessToken(): Promise<{ + token?: string | null; + res?: GaxiosResponse | null; + }>; + /** * Sets the auth credentials. */ diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts new file mode 100644 index 00000000..6b873488 --- /dev/null +++ b/src/auth/awsclient.ts @@ -0,0 +1,259 @@ +// Copyright 2021 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. + +import {GaxiosOptions} from 'gaxios'; + +import {AwsRequestSigner} from './awsrequestsigner'; +import { + BaseExternalAccountClient, + BaseExternalAccountClientOptions, +} from './baseexternalclient'; +import {RefreshOptions} from './oauth2client'; + +/** + * AWS credentials JSON interface. This is used for AWS workloads. + */ +export interface AwsClientOptions extends BaseExternalAccountClientOptions { + credential_source: { + environment_id: string; + // Region can also be determined from the AWS_REGION environment variable. + region_url?: string; + // The url field is used to determine the AWS security credentials. + // This is optional since these credentials can be retrieved from the + // AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN + // environment variables. + url?: string; + regional_cred_verification_url: string; + }; +} + +/** + * Interface defining the AWS security-credentials endpoint response. + */ +interface AwsSecurityCredentials { + Code: string; + LastUpdated: string; + Type: string; + AccessKeyId: string; + SecretAccessKey: string; + Token: string; + Expiration: string; +} + +/** + * AWS external account client. This is used for AWS workloads, where + * AWS STS GetCallerIdentity serialized signed requests are exchanged for + * GCP access token. + */ +export class AwsClient extends BaseExternalAccountClient { + private readonly environmentId: string; + private readonly regionUrl?: string; + private readonly securityCredentialsUrl?: string; + private readonly regionalCredVerificationUrl: string; + private awsRequestSigner: AwsRequestSigner | null; + private region: string; + + /** + * Instantiates an AwsClient instance using the provided JSON + * object loaded from an external account credentials file. + * An error is thrown if the credential is not a valid AWS credential. + * @param options The external account options object typically loaded + * from the external account JSON credential file. + * @param additionalOptions Optional additional behavior customization + * options. These currently customize expiration threshold time and + * whether to retry on 401/403 API request errors. + */ + constructor(options: AwsClientOptions, additionalOptions?: RefreshOptions) { + super(options, additionalOptions); + this.environmentId = options.credential_source.environment_id; + // This is only required if the AWS region is not available in the + // AWS_REGION environment variable + this.regionUrl = options.credential_source.region_url; + // This is only required if AWS security credentials are not available in + // environment variables. + this.securityCredentialsUrl = options.credential_source.url; + this.regionalCredVerificationUrl = + options.credential_source.regional_cred_verification_url; + const match = this.environmentId?.match(/^(aws)(\d+)$/); + if (!match || !this.regionalCredVerificationUrl) { + throw new Error('No valid AWS "credential_source" provided'); + } else if (parseInt(match[2], 10) !== 1) { + throw new Error( + `aws version "${match[2]}" is not supported in the current build.` + ); + } + this.awsRequestSigner = null; + this.region = ''; + } + + /** + * Triggered when an external subject token is needed to be exchanged for a + * GCP access token via GCP STS endpoint. + * This uses the `options.credential_source` object to figure out how + * to retrieve the token using the current environment. In this case, + * this uses a serialized AWS signed request to the STS GetCallerIdentity + * endpoint. + * The logic is summarized as: + * 1. Retrieve AWS region from availability-zone. + * 2a. Check AWS credentials in environment variables. If not found, get + * from security-credentials endpoint. + * 2b. Get AWS credentials from security-credentials endpoint. In order + * to retrieve this, the AWS role needs to be determined by calling + * security-credentials endpoint without any argument. Then the + * credentials can be retrieved via: security-credentials/role_name + * 3. Generate the signed request to AWS STS GetCallerIdentity action. + * 4. Inject x-goog-cloud-target-resource into header and serialize the + * signed request. This will be the subject-token to pass to GCP STS. + * @return A promise that resolves with the external subject token. + */ + async retrieveSubjectToken(): Promise { + // Initialize AWS request signer if not already initialized. + if (!this.awsRequestSigner) { + this.region = await this.getAwsRegion(); + this.awsRequestSigner = new AwsRequestSigner(async () => { + // Check environment variables for permanent credentials first. + // https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html + if ( + process.env['AWS_ACCESS_KEY_ID'] && + process.env['AWS_SECRET_ACCESS_KEY'] + ) { + return { + accessKeyId: process.env['AWS_ACCESS_KEY_ID']!, + secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY']!, + // This is normally not available for permanent credentials. + token: process.env['AWS_SESSION_TOKEN'], + }; + } + // Since the role on a VM can change, we don't need to cache it. + const roleName = await this.getAwsRoleName(); + // Temporary credentials typically last for several hours. + // Expiration is returned in response. + // Consider future optimization of this logic to cache AWS tokens + // until their natural expiration. + const awsCreds = await this.getAwsSecurityCredentials(roleName); + return { + accessKeyId: awsCreds.AccessKeyId, + secretAccessKey: awsCreds.SecretAccessKey, + token: awsCreds.Token, + }; + }, this.region); + } + + // Generate signed request to AWS STS GetCallerIdentity API. + // Use the required regional endpoint. Otherwise, the request will fail. + const options = await this.awsRequestSigner.getRequestOptions({ + url: this.regionalCredVerificationUrl.replace('{region}', this.region), + method: 'POST', + }); + // The GCP STS endpoint expects the headers to be formatted as: + // [ + // {key: 'x-amz-date', value: '...'}, + // {key: 'Authorization', value: '...'}, + // ... + // ] + // And then serialized as: + // encodeURIComponent(JSON.stringify({ + // url: '...', + // method: 'POST', + // headers: [{key: 'x-amz-date', value: '...'}, ...] + // })) + const reformattedHeader: {key: string; value: string}[] = []; + const extendedHeaders = Object.assign( + { + // The full, canonical resource name of the workload identity pool + // provider, with or without the HTTPS prefix. + // Including this header as part of the signature is recommended to + // ensure data integrity. + 'x-goog-cloud-target-resource': this.audience, + }, + options.headers + ); + // Reformat header to GCP STS expected format. + for (const key in extendedHeaders) { + reformattedHeader.push({ + key, + value: extendedHeaders[key], + }); + } + // Serialize the reformatted signed request. + return encodeURIComponent( + JSON.stringify({ + url: options.url, + method: options.method, + headers: reformattedHeader, + }) + ); + } + + /** + * @return A promise that resolves with the current AWS region. + */ + private async getAwsRegion(): Promise { + if (process.env['AWS_REGION']) { + return process.env['AWS_REGION']; + } + if (!this.regionUrl) { + throw new Error( + 'Unable to determine AWS region due to missing ' + + '"options.credential_source.region_url"' + ); + } + const opts: GaxiosOptions = { + url: this.regionUrl, + method: 'GET', + responseType: 'text', + }; + const response = await this.transporter.request(opts); + // Remove last character. For example, if us-east-2b is returned, + // the region would be us-east-2. + return response.data.substr(0, response.data.length - 1); + } + + /** + * @return A promise that resolves with the assigned role to the current + * AWS VM. This is needed for calling the security-credentials endpoint. + */ + private async getAwsRoleName(): Promise { + if (!this.securityCredentialsUrl) { + throw new Error( + 'Unable to determine AWS role name due to missing ' + + '"options.credential_source.url"' + ); + } + const opts: GaxiosOptions = { + url: this.securityCredentialsUrl, + method: 'GET', + responseType: 'text', + }; + const response = await this.transporter.request(opts); + return response.data; + } + + /** + * Retrieves the temporary AWS credentials by calling the security-credentials + * endpoint as specified in the `credential_source` object. + * @param roleName The role attached to the current VM. + * @return A promise that resolves with the temporary AWS credentials + * needed for creating the GetCallerIdentity signed request. + */ + private async getAwsSecurityCredentials( + roleName: string + ): Promise { + const response = await this.transporter.request({ + url: `${this.securityCredentialsUrl}/${roleName}`, + responseType: 'json', + }); + return response.data; + } +} diff --git a/src/auth/awsrequestsigner.ts b/src/auth/awsrequestsigner.ts new file mode 100644 index 00000000..a0e8870c --- /dev/null +++ b/src/auth/awsrequestsigner.ts @@ -0,0 +1,305 @@ +// Copyright 2021 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. + +import {GaxiosOptions} from 'gaxios'; + +import {Headers} from './oauth2client'; +import {Crypto, createCrypto, fromArrayBufferToHex} from '../crypto/crypto'; + +type HttpMethod = + | 'GET' + | 'POST' + | 'PUT' + | 'PATCH' + | 'HEAD' + | 'DELETE' + | 'CONNECT' + | 'OPTIONS' + | 'TRACE'; + +/** Interface defining the AWS authorization header map for signed requests. */ +interface AwsAuthHeaderMap { + amzDate?: string; + authorizationHeader: string; + canonicalQuerystring: string; +} + +/** + * Interface defining AWS security credentials. + * These are either determined from AWS security_credentials endpoint or + * AWS environment variables. + */ +interface AwsSecurityCredentials { + accessKeyId: string; + secretAccessKey: string; + token?: string; +} + +/** + * Interface defining the parameters needed to compute the AWS + * authentication header map. + */ +interface GenerateAuthHeaderMapOptions { + // The crypto instance used to facilitate cryptographic operations. + crypto: Crypto; + // The AWS service URL hostname. + host: string; + // The AWS service URL path name. + canonicalUri: string; + // The AWS service URL query string. + canonicalQuerystring: string; + // The HTTP method used to call this API. + method: HttpMethod; + // The AWS region. + region: string; + // The AWS security credentials. + securityCredentials: AwsSecurityCredentials; + // The optional request payload if available. + requestPayload?: string; + // The optional additional headers needed for the requested AWS API. + additionalAmzHeaders?: Headers; +} + +/** AWS Signature Version 4 signing algorithm identifier. */ +const AWS_ALGORITHM = 'AWS4-HMAC-SHA256'; +/** + * The termination string for the AWS credential scope value as defined in + * https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + */ +const AWS_REQUEST_TYPE = 'aws4_request'; + +/** + * Implements an AWS API request signer based on the AWS Signature Version 4 + * signing process. + * https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + */ +export class AwsRequestSigner { + private readonly crypto: Crypto; + + /** + * Instantiates an AWS API request signer used to send authenticated signed + * requests to AWS APIs based on the AWS Signature Version 4 signing process. + * This also provides a mechanism to generate the signed request without + * sending it. + * @param getCredentials A mechanism to retrieve AWS security credentials + * when needed. + * @param region The AWS region to use. + */ + constructor( + private readonly getCredentials: () => Promise, + private readonly region: string + ) { + this.crypto = createCrypto(); + } + + /** + * Generates the signed request for the provided HTTP request for calling + * an AWS API. This follows the steps described at: + * https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html + * @param amzOptions The AWS request options that need to be signed. + * @return A promise that resolves with the GaxiosOptions containing the + * signed HTTP request parameters. + */ + async getRequestOptions(amzOptions: GaxiosOptions): Promise { + if (!amzOptions.url) { + throw new Error('"url" is required in "amzOptions"'); + } + // Stringify JSON requests. This will be set in the request body of the + // generated signed request. + const requestPayloadData = + typeof amzOptions.data === 'object' + ? JSON.stringify(amzOptions.data) + : amzOptions.data; + const url = amzOptions.url; + const method = amzOptions.method || 'GET'; + const requestPayload = amzOptions.body || requestPayloadData; + const additionalAmzHeaders = amzOptions.headers; + const awsSecurityCredentials = await this.getCredentials(); + const uri = new URL(url); + const headerMap = await generateAuthenticationHeaderMap({ + crypto: this.crypto, + host: uri.host, + canonicalUri: uri.pathname, + canonicalQuerystring: uri.search.substr(1), + method, + region: this.region, + securityCredentials: awsSecurityCredentials, + requestPayload, + additionalAmzHeaders, + }); + // Append additional optional headers, eg. X-Amz-Target, Content-Type, etc. + const headers: {[key: string]: string} = Object.assign( + // Add x-amz-date if available. + headerMap.amzDate ? {'x-amz-date': headerMap.amzDate} : {}, + { + Authorization: headerMap.authorizationHeader, + host: uri.host, + }, + additionalAmzHeaders || {} + ); + if (awsSecurityCredentials.token) { + Object.assign(headers, { + 'x-amz-security-token': awsSecurityCredentials.token, + }); + } + const awsSignedReq: GaxiosOptions = { + url, + method: method, + headers, + }; + + if (typeof requestPayload !== 'undefined') { + awsSignedReq.body = requestPayload; + } + + return awsSignedReq; + } +} + +/** + * Creates the HMAC-SHA256 hash of the provided message using the + * provided key. + * + * @param crypto The crypto instance used to facilitate cryptographic + * operations. + * @param key The HMAC-SHA256 key to use. + * @param msg The message to hash. + * @return The computed hash bytes. + */ +async function sign( + crypto: Crypto, + key: string | ArrayBuffer, + msg: string +): Promise { + return await crypto.signWithHmacSha256(key, msg); +} + +/** + * Calculates the signing key used to calculate the signature for + * AWS Signature Version 4 based on: + * https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html + * + * @param crypto The crypto instance used to facilitate cryptographic + * operations. + * @param key The AWS secret access key. + * @param dateStamp The '%Y%m%d' date format. + * @param region The AWS region. + * @param serviceName The AWS service name, eg. sts. + * @return The signing key bytes. + */ +async function getSigningKey( + crypto: Crypto, + key: string, + dateStamp: string, + region: string, + serviceName: string +): Promise { + const kDate = await sign(crypto, `AWS4${key}`, dateStamp); + const kRegion = await sign(crypto, kDate, region); + const kService = await sign(crypto, kRegion, serviceName); + const kSigning = await sign(crypto, kService, 'aws4_request'); + return kSigning; +} + +/** + * Generates the authentication header map needed for generating the AWS + * Signature Version 4 signed request. + * + * @param option The options needed to compute the authentication header map. + * @return The AWS authentication header map which constitutes of the following + * components: amz-date, authorization header and canonical query string. + */ +async function generateAuthenticationHeaderMap( + options: GenerateAuthHeaderMapOptions +): Promise { + const additionalAmzHeaders = options.additionalAmzHeaders || {}; + const requestPayload = options.requestPayload || ''; + // iam.amazonaws.com host => iam service. + // sts.us-east-2.amazonaws.com => sts service. + const serviceName = options.host.split('.')[0]; + const now = new Date(); + // Format: '%Y%m%dT%H%M%SZ'. + const amzDate = now + .toISOString() + .replace(/[-:]/g, '') + .replace(/\.[0-9]+/, ''); + // Format: '%Y%m%d'. + const dateStamp = now.toISOString().replace(/[-]/g, '').replace(/T.*/, ''); + + // Change all additional headers to be lower case. + const reformattedAdditionalAmzHeaders: Headers = {}; + Object.keys(additionalAmzHeaders).forEach(key => { + reformattedAdditionalAmzHeaders[key.toLowerCase()] = + additionalAmzHeaders[key]; + }); + // Add AWS token if available. + if (options.securityCredentials.token) { + reformattedAdditionalAmzHeaders['x-amz-security-token'] = + options.securityCredentials.token; + } + // Header keys need to be sorted alphabetically. + const amzHeaders = Object.assign( + { + host: options.host, + }, + // Previously the date was not fixed with x-amz- and could be provided manually. + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req + reformattedAdditionalAmzHeaders.date ? {} : {'x-amz-date': amzDate}, + reformattedAdditionalAmzHeaders + ); + let canonicalHeaders = ''; + const signedHeadersList = Object.keys(amzHeaders).sort(); + signedHeadersList.forEach(key => { + canonicalHeaders += `${key}:${amzHeaders[key]}\n`; + }); + const signedHeaders = signedHeadersList.join(';'); + + const payloadHash = await options.crypto.sha256DigestHex(requestPayload); + // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + const canonicalRequest = + `${options.method}\n` + + `${options.canonicalUri}\n` + + `${options.canonicalQuerystring}\n` + + `${canonicalHeaders}\n` + + `${signedHeaders}\n` + + `${payloadHash}`; + const credentialScope = `${dateStamp}/${options.region}/${serviceName}/${AWS_REQUEST_TYPE}`; + // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + const stringToSign = + `${AWS_ALGORITHM}\n` + + `${amzDate}\n` + + `${credentialScope}\n` + + (await options.crypto.sha256DigestHex(canonicalRequest)); + // https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html + const signingKey = await getSigningKey( + options.crypto, + options.securityCredentials.secretAccessKey, + dateStamp, + options.region, + serviceName + ); + const signature = await sign(options.crypto, signingKey, stringToSign); + // https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html + const authorizationHeader = + `${AWS_ALGORITHM} Credential=${options.securityCredentials.accessKeyId}/` + + `${credentialScope}, SignedHeaders=${signedHeaders}, ` + + `Signature=${fromArrayBufferToHex(signature)}`; + + return { + // Do not return x-amz-date if date is available. + amzDate: reformattedAdditionalAmzHeaders.date ? undefined : amzDate, + authorizationHeader, + canonicalQuerystring: options.canonicalQuerystring, + }; +} diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts new file mode 100644 index 00000000..2d0aa10c --- /dev/null +++ b/src/auth/baseexternalclient.ts @@ -0,0 +1,487 @@ +// Copyright 2021 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. + +import { + GaxiosError, + GaxiosOptions, + GaxiosPromise, + GaxiosResponse, +} from 'gaxios'; +import * as stream from 'stream'; + +import {Credentials} from './credentials'; +import {AuthClient} from './authclient'; +import {BodyResponseCallback} from '../transporters'; +import {GetAccessTokenResponse, Headers, RefreshOptions} from './oauth2client'; +import * as sts from './stscredentials'; +import {ClientAuthentication} from './oauth2common'; + +/** + * The required token exchange grant_type: rfc8693#section-2.1 + */ +const STS_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'; +/** + * The requested token exchange requested_token_type: rfc8693#section-2.1 + */ +const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; +/** The default OAuth scope to request when none is provided. */ +const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'; +/** + * Offset to take into account network delays and server clock skews. + */ +export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000; +/** + * The credentials JSON file type for external account clients. + * There are 3 types of JSON configs: + * 1. authorized_user => Google end user credential + * 2. service_account => Google service account credential + * 3. external_Account => non-GCP service (eg. AWS, Azure, K8s) + */ +export const EXTERNAL_ACCOUNT_TYPE = 'external_account'; +/** Cloud resource manager URL used to retrieve project information. */ +export const CLOUD_RESOURCE_MANAGER = + 'https://cloudresourcemanager.googleapis.com/v1/projects/'; + +/** + * Base external account credentials json interface. + */ +export interface BaseExternalAccountClientOptions { + type: string; + audience: string; + subject_token_type: string; + service_account_impersonation_url?: string; + token_url: string; + token_info_url?: string; + client_id?: string; + client_secret?: string; + quota_project_id?: string; +} + +/** + * Interface defining the successful response for iamcredentials + * generateAccessToken API. + * https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken + */ +export interface IamGenerateAccessTokenResponse { + accessToken: string; + // ISO format used for expiration time: 2014-10-02T15:01:23.045123456Z + expireTime: string; +} + +/** + * Interface defining the project information response returned by the cloud + * resource manager. + * https://cloud.google.com/resource-manager/reference/rest/v1/projects#Project + */ +export interface ProjectInfo { + projectNumber: string; + projectId: string; + lifecycleState: string; + name: string; + createTime?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parent: {[key: string]: any}; +} + +/** + * Internal interface for tracking the access token expiration time. + */ +interface CredentialsWithResponse extends Credentials { + res?: GaxiosResponse | null; +} + +/** + * Base external account client. This is used to instantiate AuthClients for + * exchanging external account credentials for GCP access token and authorizing + * requests to GCP APIs. + * The base class implements common logic for exchanging various type of + * external credentials for GCP access token. The logic of determining and + * retrieving the external credential based on the environment and + * credential_source will be left for the subclasses. + */ +export abstract class BaseExternalAccountClient extends AuthClient { + /** + * OAuth scopes for the GCP access token to use. When not provided, + * the default https://www.googleapis.com/auth/cloud-platform is + * used. + */ + public scopes?: string | string[]; + private cachedAccessToken: CredentialsWithResponse | null; + protected readonly audience: string; + private readonly subjectTokenType: string; + private readonly serviceAccountImpersonationUrl?: string; + private readonly stsCredential: sts.StsCredentials; + public projectId: string | null; + public projectNumber: string | null; + public readonly eagerRefreshThresholdMillis: number; + public readonly forceRefreshOnFailure: boolean; + + /** + * Instantiate a BaseExternalAccountClient instance using the provided JSON + * object loaded from an external account credentials file. + * @param options The external account options object typically loaded + * from the external account JSON credential file. + * @param additionalOptions Optional additional behavior customization + * options. These currently customize expiration threshold time and + * whether to retry on 401/403 API request errors. + */ + constructor( + options: BaseExternalAccountClientOptions, + additionalOptions?: RefreshOptions + ) { + super(); + if (options.type !== EXTERNAL_ACCOUNT_TYPE) { + throw new Error( + `Expected "${EXTERNAL_ACCOUNT_TYPE}" type but ` + + `received "${options.type}"` + ); + } + const clientAuth = options.client_id + ? ({ + confidentialClientType: 'basic', + clientId: options.client_id, + clientSecret: options.client_secret, + } as ClientAuthentication) + : undefined; + this.stsCredential = new sts.StsCredentials(options.token_url, clientAuth); + // Default OAuth scope. This could be overridden via public property. + this.scopes = [DEFAULT_OAUTH_SCOPE]; + this.cachedAccessToken = null; + this.audience = options.audience; + this.subjectTokenType = options.subject_token_type; + this.quotaProjectId = options.quota_project_id; + this.serviceAccountImpersonationUrl = + options.service_account_impersonation_url; + // As threshold could be zero, + // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the + // zero value. + if (typeof additionalOptions?.eagerRefreshThresholdMillis !== 'number') { + this.eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET; + } else { + this.eagerRefreshThresholdMillis = additionalOptions! + .eagerRefreshThresholdMillis as number; + } + this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; + this.projectId = null; + this.projectNumber = this.getProjectNumber(this.audience); + } + + /** + * Provides a mechanism to inject GCP access tokens directly. + * When the provided credential expires, a new credential, using the + * external account options, is retrieved. + * @param credentials The Credentials object to set on the current client. + */ + setCredentials(credentials: Credentials) { + super.setCredentials(credentials); + this.cachedAccessToken = credentials; + } + + /** + * Triggered when a external subject token is needed to be exchanged for a GCP + * access token via GCP STS endpoint. + * This abstract method needs to be implemented by subclasses depending on + * the type of external credential used. + * @return A promise that resolves with the external subject token. + */ + abstract async retrieveSubjectToken(): Promise; + + /** + * @return A promise that resolves with the current GCP access token + * response. If the current credential is expired, a new one is retrieved. + */ + async getAccessToken(): Promise { + // If cached access token is unavailable or expired, force refresh. + if (!this.cachedAccessToken || this.isExpired(this.cachedAccessToken)) { + await this.refreshAccessTokenAsync(); + } + // Return GCP access token in GetAccessTokenResponse format. + return { + token: this.cachedAccessToken!.access_token, + res: this.cachedAccessToken!.res, + }; + } + + /** + * The main authentication interface. It takes an optional url which when + * present is the endpoint> being accessed, and returns a Promise which + * resolves with authorization header fields. + * + * The result has the form: + * { Authorization: 'Bearer ' } + */ + async getRequestHeaders(): Promise { + const accessTokenResponse = await this.getAccessToken(); + const headers: Headers = { + Authorization: `Bearer ${accessTokenResponse.token}`, + }; + return this.addSharedMetadataHeaders(headers); + } + + /** + * Provides a request implementation with OAuth 2.0 flow. In cases of + * HTTP 401 and 403 responses, it automatically asks for a new access token + * and replays the unsuccessful request. + * @param opts Request options. + * @param callback callback. + * @return A promise that resolves with the HTTP response when no callback is + * provided. + */ + request(opts: GaxiosOptions): GaxiosPromise; + request(opts: GaxiosOptions, callback: BodyResponseCallback): void; + request( + opts: GaxiosOptions, + callback?: BodyResponseCallback + ): GaxiosPromise | void { + if (callback) { + this.requestAsync(opts).then( + r => callback(null, r), + e => { + return callback(e, e.response); + } + ); + } else { + return this.requestAsync(opts); + } + } + + /** + * @return A promise that resolves with the project ID corresponding to the + * current workload identity pool. When not determinable, this resolves with + * null. + * This is introduced to match the current pattern of using the Auth + * library: + * const projectId = await auth.getProjectId(); + * const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; + * const res = await client.request({ url }); + * The resource may not have permission + * (resourcemanager.projects.get) to call this API or the required + * scopes may not be selected: + * https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes + */ + async getProjectId(): Promise { + if (this.projectId) { + // Return previously determined project ID. + return this.projectId; + } else if (this.projectNumber) { + // Preferable not to use request() to avoid retrial policies. + const headers = await this.getRequestHeaders(); + const response = await this.transporter.request({ + headers, + url: `${CLOUD_RESOURCE_MANAGER}${this.projectNumber}`, + responseType: 'json', + }); + this.projectId = response.data.projectId; + return this.projectId; + } + return null; + } + + /** + * Authenticates the provided HTTP request, processes it and resolves with the + * returned response. + * @param opts The HTTP request options. + * @param retry Whether the current attempt is a retry after a failed attempt. + * @return A promise that resolves with the successful response. + */ + protected async requestAsync( + opts: GaxiosOptions, + retry = false + ): Promise> { + let response: GaxiosResponse; + try { + const requestHeaders = await this.getRequestHeaders(); + opts.headers = opts.headers || {}; + if (requestHeaders && requestHeaders['x-goog-user-project']) { + opts.headers['x-goog-user-project'] = + requestHeaders['x-goog-user-project']; + } + if (requestHeaders && requestHeaders.Authorization) { + opts.headers.Authorization = requestHeaders.Authorization; + } + response = await this.transporter.request(opts); + } catch (e) { + const res = (e as GaxiosError).response; + if (res) { + const statusCode = res.status; + // Retry the request for metadata if the following criteria are true: + // - We haven't already retried. It only makes sense to retry once. + // - The response was a 401 or a 403 + // - The request didn't send a readableStream + // - forceRefreshOnFailure is true + const isReadableStream = res.config.data instanceof stream.Readable; + const isAuthErr = statusCode === 401 || statusCode === 403; + if ( + !retry && + isAuthErr && + !isReadableStream && + this.forceRefreshOnFailure + ) { + await this.refreshAccessTokenAsync(); + return await this.requestAsync(opts, true); + } + } + throw e; + } + return response; + } + + /** + * Forces token refresh, even if unexpired tokens are currently cached. + * External credentials are exchanged for GCP access tokens via the token + * exchange endpoint and other settings provided in the client options + * object. + * If the service_account_impersonation_url is provided, an additional + * step to exchange the external account GCP access token for a service + * account impersonated token is performed. + * @return A promise that resolves with the fresh GCP access tokens. + */ + protected async refreshAccessTokenAsync(): Promise { + // Retrieve the external credential. + const subjectToken = await this.retrieveSubjectToken(); + // Construct the STS credentials options. + const stsCredentialsOptions: sts.StsCredentialsOptions = { + grantType: STS_GRANT_TYPE, + audience: this.audience, + requestedTokenType: STS_REQUEST_TOKEN_TYPE, + subjectToken, + subjectTokenType: this.subjectTokenType, + // generateAccessToken requires the provided access token to have + // scopes: + // https://www.googleapis.com/auth/iam or + // https://www.googleapis.com/auth/cloud-platform + // The new service account access token scopes will match the user + // provided ones. + scope: this.serviceAccountImpersonationUrl + ? [DEFAULT_OAUTH_SCOPE] + : this.getScopesArray(), + }; + + // Exchange the external credentials for a GCP access token. + const stsResponse = await this.stsCredential.exchangeToken( + stsCredentialsOptions + ); + + if (this.serviceAccountImpersonationUrl) { + this.cachedAccessToken = await this.getImpersonatedAccessToken( + stsResponse.access_token + ); + } else { + // Save response in cached access token. + this.cachedAccessToken = { + access_token: stsResponse.access_token, + expiry_date: new Date().getTime() + stsResponse.expires_in * 1000, + res: stsResponse.res, + }; + } + + // Save credentials. + this.credentials = {}; + Object.assign(this.credentials, this.cachedAccessToken); + delete (this.credentials as CredentialsWithResponse).res; + + // Trigger tokens event to notify external listeners. + this.emit('tokens', { + refresh_token: null, + expiry_date: this.cachedAccessToken!.expiry_date, + access_token: this.cachedAccessToken!.access_token, + token_type: 'Bearer', + id_token: null, + }); + // Return the cached access token. + return this.cachedAccessToken; + } + + /** + * Returns the workload identity pool project number if it is determinable + * from the audience resource name. + * @param audience The STS audience used to determine the project number. + * @return The project number associated with the workload identity pool, if + * this can be determined from the STS audience field. Otherwise, null is + * returned. + */ + private getProjectNumber(audience: string): string | null { + // STS audience pattern: + // //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/... + const match = audience.match(/\/projects\/([^/]+)/); + if (!match) { + return null; + } + return match[1]; + } + + /** + * Exchanges an external account GCP access token for a service + * account impersonated access token using iamcredentials + * GenerateAccessToken API. + * @param token The access token to exchange for a service account access + * token. + * @return A promise that resolves with the service account impersonated + * credentials response. + */ + private async getImpersonatedAccessToken( + token: string + ): Promise { + const opts: GaxiosOptions = { + url: this.serviceAccountImpersonationUrl!, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + data: { + scope: this.getScopesArray(), + }, + responseType: 'json', + }; + const response = await this.transporter.request( + opts + ); + const successResponse = response.data; + return { + access_token: successResponse.accessToken, + // Convert from ISO format to timestamp. + expiry_date: new Date(successResponse.expireTime).getTime(), + res: response, + }; + } + + /** + * Returns whether the provided credentials are expired or not. + * If there is no expiry time, assumes the token is not expired or expiring. + * @param accessToken The credentials to check for expiration. + * @return Whether the credentials are expired or not. + */ + private isExpired(accessToken: Credentials): boolean { + const now = new Date().getTime(); + return accessToken.expiry_date + ? now >= accessToken.expiry_date - this.eagerRefreshThresholdMillis + : false; + } + + /** + * @return The list of scopes for the requested GCP access token. + */ + private getScopesArray(): string[] { + // Since scopes can be provided as string or array, the type should + // be normalized. + if (typeof this.scopes === 'string') { + return [this.scopes]; + } else if (typeof this.scopes === 'undefined') { + return [DEFAULT_OAUTH_SCOPE]; + } else { + return this.scopes; + } + } +} diff --git a/src/auth/externalclient.ts b/src/auth/externalclient.ts new file mode 100644 index 00000000..191c6953 --- /dev/null +++ b/src/auth/externalclient.ts @@ -0,0 +1,80 @@ +// Copyright 2021 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. + +import {RefreshOptions} from './oauth2client'; +import { + BaseExternalAccountClient, + // This is the identifier in the JSON config for the type of credential. + // This string constant indicates that an external account client should be + // instantiated. + // There are 3 types of JSON configs: + // 1. authorized_user => Google end user credential + // 2. service_account => Google service account credential + // 3. external_Account => non-GCP service (eg. AWS, Azure, K8s) + EXTERNAL_ACCOUNT_TYPE, +} from './baseexternalclient'; +import { + IdentityPoolClient, + IdentityPoolClientOptions, +} from './identitypoolclient'; +import {AwsClient, AwsClientOptions} from './awsclient'; + +export type ExternalAccountClientOptions = + | IdentityPoolClientOptions + | AwsClientOptions; + +/** + * Dummy class with no constructor. Developers are expected to use fromJSON. + */ +export class ExternalAccountClient { + constructor() { + throw new Error( + 'ExternalAccountClients should be initialized via: ' + + 'ExternalAccountClient.fromJSON(), ' + + 'directly via explicit constructors, eg. ' + + 'new AwsClient(options), new IdentityPoolClient(options) or via ' + + 'new GoogleAuth(options).getClient()' + ); + } + + /** + * This static method will instantiate the + * corresponding type of external account credential depending on the + * underlying credential source. + * @param options The external account options object typically loaded + * from the external account JSON credential file. + * @param additionalOptions Optional additional behavior customization + * options. These currently customize expiration threshold time and + * whether to retry on 401/403 API request errors. + * @return A BaseExternalAccountClient instance or null if the options + * provided do not correspond to an external account credential. + */ + static fromJSON( + options: ExternalAccountClientOptions, + additionalOptions?: RefreshOptions + ): BaseExternalAccountClient | null { + if (options && options.type === EXTERNAL_ACCOUNT_TYPE) { + if ((options as AwsClientOptions).credential_source?.environment_id) { + return new AwsClient(options as AwsClientOptions, additionalOptions); + } else { + return new IdentityPoolClient( + options as IdentityPoolClientOptions, + additionalOptions + ); + } + } else { + return null; + } + } +} diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 437677ab..cd6f6040 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -28,35 +28,41 @@ import {CredentialBody, JWTInput} from './credentials'; import {IdTokenClient} from './idtokenclient'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; -import { - Headers, - OAuth2Client, - OAuth2ClientOptions, - RefreshOptions, -} from './oauth2client'; +import {Headers, OAuth2ClientOptions, RefreshOptions} from './oauth2client'; import {UserRefreshClient, UserRefreshClientOptions} from './refreshclient'; +import { + ExternalAccountClient, + ExternalAccountClientOptions, +} from './externalclient'; +import { + EXTERNAL_ACCOUNT_TYPE, + BaseExternalAccountClient, +} from './baseexternalclient'; +import {AuthClient} from './authclient'; + +/** + * Defines all types of explicit clients that are determined via ADC JSON + * config file. + */ +export type JSONClient = JWT | UserRefreshClient | BaseExternalAccountClient; export interface ProjectIdCallback { (err?: Error | null, projectId?: string | null): void; } export interface CredentialCallback { - (err: Error | null, result?: UserRefreshClient | JWT): void; + (err: Error | null, result?: JSONClient): void; } // eslint-disable-next-line @typescript-eslint/no-empty-interface interface DeprecatedGetClientOptions {} export interface ADCCallback { - ( - err: Error | null, - credential?: OAuth2Client, - projectId?: string | null - ): void; + (err: Error | null, credential?: AuthClient, projectId?: string | null): void; } export interface ADCResponse { - credential: OAuth2Client; + credential: AuthClient; projectId: string | null; } @@ -72,9 +78,10 @@ export interface GoogleAuthOptions { keyFile?: string; /** - * Object containing client_email and private_key properties + * Object containing client_email and private_key properties, or the + * external account client options. */ - credentials?: CredentialBody; + credentials?: CredentialBody | ExternalAccountClientOptions; /** * Options object passed to the constructor of the client @@ -115,9 +122,9 @@ export class GoogleAuth { private _cachedProjectId?: string | null; // To save the contents of the JSON credential file - jsonContent: JWTInput | null = null; + jsonContent: JWTInput | ExternalAccountClientOptions | null = null; - cachedCredential: JWT | UserRefreshClient | Compute | null = null; + cachedCredential: JSONClient | Compute | null = null; /** * Scopes populated by the client library by default. We differentiate between @@ -180,7 +187,8 @@ export class GoogleAuth { this.getProductionProjectId() || (await this.getFileProjectId()) || (await this.getDefaultServiceProjectId()) || - (await this.getGCEProjectId()); + (await this.getGCEProjectId()) || + (await this.getExternalAccountClientProjectId()); this._cachedProjectId = projectId; if (!projectId) { throw new Error( @@ -199,6 +207,14 @@ export class GoogleAuth { 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. + */ + private getAnyScopes(): string | string[] | undefined { + return this.scopes || this.defaultScopes; + } + /** * Obtains the default service-level credentials for the application. * @param callback Optional callback. @@ -235,12 +251,12 @@ export class GoogleAuth { // If we've already got a cached credential, just return it. if (this.cachedCredential) { return { - credential: this.cachedCredential as JWT | UserRefreshClient, + credential: this.cachedCredential as JSONClient, projectId: await this.getProjectIdAsync(), }; } - let credential: JWT | UserRefreshClient | null; + let credential: JSONClient | null; let projectId: string | null; // Check for the existence of a local environment variable pointing to the // location of the credential file. This is typically used in local @@ -252,6 +268,8 @@ export class GoogleAuth { if (credential instanceof JWT) { credential.defaultScopes = this.defaultScopes; credential.scopes = this.scopes; + } else if (credential instanceof BaseExternalAccountClient) { + credential.scopes = this.getAnyScopes(); } this.cachedCredential = credential; projectId = await this.getProjectId(); @@ -266,6 +284,8 @@ export class GoogleAuth { if (credential instanceof JWT) { credential.defaultScopes = this.defaultScopes; credential.scopes = this.scopes; + } else if (credential instanceof BaseExternalAccountClient) { + credential.scopes = this.getAnyScopes(); } this.cachedCredential = credential; projectId = await this.getProjectId(); @@ -290,7 +310,7 @@ export class GoogleAuth { // For GCE, just return a default ComputeClient. It will take care of // the rest. - (options as ComputeOptions).scopes = this.scopes || this.defaultScopes; + (options as ComputeOptions).scopes = this.getAnyScopes(); this.cachedCredential = new Compute(options); projectId = await this.getProjectId(); return {projectId, credential: this.cachedCredential}; @@ -315,7 +335,7 @@ export class GoogleAuth { */ async _tryGetApplicationCredentialsFromEnvironmentVariable( options?: RefreshOptions - ): Promise { + ): Promise { const credentialsPath = process.env['GOOGLE_APPLICATION_CREDENTIALS'] || process.env['google_application_credentials']; @@ -340,7 +360,7 @@ export class GoogleAuth { */ async _tryGetApplicationCredentialsFromWellKnownFile( options?: RefreshOptions - ): Promise { + ): Promise { // First, figure out the location of the file, depending upon the OS type. let location = null; if (this._isWindows()) { @@ -385,7 +405,7 @@ export class GoogleAuth { async _getApplicationCredentialsFromFilePath( filePath: string, options: RefreshOptions = {} - ): Promise { + ): Promise { // Make sure the path looks like a string. if (!filePath || filePath.length === 0) { throw new Error('The file path is invalid.'); @@ -417,8 +437,8 @@ export class GoogleAuth { * @param options The JWT or UserRefresh options for the client * @returns JWT or UserRefresh Client with data */ - fromJSON(json: JWTInput, options?: RefreshOptions): JWT | UserRefreshClient { - let client: UserRefreshClient | JWT; + fromJSON(json: JWTInput, options?: RefreshOptions): JSONClient { + let client: JSONClient; if (!json) { throw new Error( 'Must pass in a JSON object containing the Google auth settings.' @@ -427,12 +447,19 @@ export class GoogleAuth { options = options || {}; if (json.type === 'authorized_user') { client = new UserRefreshClient(options); + client.fromJSON(json); + } else if (json.type === EXTERNAL_ACCOUNT_TYPE) { + client = ExternalAccountClient.fromJSON( + json as ExternalAccountClientOptions, + options + )!; + client.scopes = this.getAnyScopes(); } else { (options as JWTOptions).scopes = this.scopes; client = new JWT(options); client.defaultScopes = this.defaultScopes; + client.fromJSON(json); } - client.fromJSON(json); return client; } @@ -446,18 +473,25 @@ export class GoogleAuth { private _cacheClientFromJSON( json: JWTInput, options?: RefreshOptions - ): JWT | UserRefreshClient { - let client: UserRefreshClient | JWT; + ): JSONClient { + let client: JSONClient; // create either a UserRefreshClient or JWT client. options = options || {}; if (json.type === 'authorized_user') { client = new UserRefreshClient(options); + client.fromJSON(json); + } else if (json.type === EXTERNAL_ACCOUNT_TYPE) { + client = ExternalAccountClient.fromJSON( + json as ExternalAccountClientOptions, + options + )!; + client.scopes = this.getAnyScopes(); } else { (options as JWTOptions).scopes = this.scopes; client = new JWT(options); client.defaultScopes = this.defaultScopes; + client.fromJSON(json); } - client.fromJSON(json); // cache both raw data used to instantiate client and client itself. this.jsonContent = json; this.cachedCredential = client; @@ -469,12 +503,12 @@ export class GoogleAuth { * @param inputStream The input stream. * @param callback Optional callback. */ - fromStream(inputStream: stream.Readable): Promise; + fromStream(inputStream: stream.Readable): Promise; fromStream(inputStream: stream.Readable, callback: CredentialCallback): void; fromStream( inputStream: stream.Readable, options: RefreshOptions - ): Promise; + ): Promise; fromStream( inputStream: stream.Readable, options: RefreshOptions, @@ -484,7 +518,7 @@ export class GoogleAuth { inputStream: stream.Readable, optionsOrCallback: RefreshOptions | CredentialCallback = {}, callback?: CredentialCallback - ): Promise | void { + ): Promise | void { let options: RefreshOptions = {}; if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; @@ -504,7 +538,7 @@ export class GoogleAuth { private fromStreamAsync( inputStream: stream.Readable, options?: RefreshOptions - ): Promise { + ): Promise { return new Promise((resolve, reject) => { if (!inputStream) { throw new Error( @@ -629,6 +663,21 @@ export class GoogleAuth { } } + /** + * Gets the project ID from external account client if available. + */ + private async getExternalAccountClientProjectId(): Promise { + if (!this.jsonContent || this.jsonContent.type !== EXTERNAL_ACCOUNT_TYPE) { + return null; + } + const creds = await this.getClient(); + try { + return await (creds as BaseExternalAccountClient).getProjectId(); + } catch (e) { + return null; + } + } + /** * Gets the Compute Engine project ID if it can be inferred. */ @@ -671,8 +720,8 @@ export class GoogleAuth { if (this.jsonContent) { const credential: CredentialBody = { - client_email: this.jsonContent.client_email, - private_key: this.jsonContent.private_key, + client_email: (this.jsonContent as JWTInput).client_email, + private_key: (this.jsonContent as JWTInput).private_key, }; return credential; } diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts new file mode 100644 index 00000000..33badafa --- /dev/null +++ b/src/auth/identitypoolclient.ts @@ -0,0 +1,219 @@ +// Copyright 2021 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. + +import {GaxiosOptions} from 'gaxios'; +import * as fs from 'fs'; +import {promisify} from 'util'; + +import { + BaseExternalAccountClient, + BaseExternalAccountClientOptions, +} from './baseexternalclient'; +import {RefreshOptions} from './oauth2client'; + +// fs.readfile is undefined in browser karma tests causing +// `npm run browser-test` to fail as test.oauth2.ts imports this file via +// src/index.ts. +// Fallback to void function to avoid promisify throwing a TypeError. +const readFile = promisify(fs.readFile ?? (() => {})); +const realpath = promisify(fs.realpath ?? (() => {})); +const lstat = promisify(fs.lstat ?? (() => {})); + +type SubjectTokenFormatType = 'json' | 'text'; + +interface SubjectTokenJsonResponse { + [key: string]: string; +} + +/** + * Url-sourced/file-sourced credentials json interface. + * This is used for K8s and Azure workloads. + */ +export interface IdentityPoolClientOptions + extends BaseExternalAccountClientOptions { + credential_source: { + file?: string; + url?: string; + headers?: { + [key: string]: string; + }; + format?: { + type: SubjectTokenFormatType; + subject_token_field_name?: string; + }; + }; +} + +/** + * Defines the Url-sourced and file-sourced external account clients mainly + * used for K8s and Azure workloads. + */ +export class IdentityPoolClient extends BaseExternalAccountClient { + private readonly file?: string; + private readonly url?: string; + private readonly headers?: {[key: string]: string}; + private readonly formatType: SubjectTokenFormatType; + private readonly formatSubjectTokenFieldName?: string; + + /** + * Instantiate an IdentityPoolClient instance using the provided JSON + * object loaded from an external account credentials file. + * An error is thrown if the credential is not a valid file-sourced or + * url-sourced credential. + * @param options The external account options object typically loaded + * from the external account JSON credential file. + * @param additionalOptions Optional additional behavior customization + * options. These currently customize expiration threshold time and + * whether to retry on 401/403 API request errors. + */ + constructor( + options: IdentityPoolClientOptions, + additionalOptions?: RefreshOptions + ) { + super(options, additionalOptions); + this.file = options.credential_source.file; + this.url = options.credential_source.url; + this.headers = options.credential_source.headers; + if (!this.file && !this.url) { + throw new Error('No valid Identity Pool "credential_source" provided'); + } + // Text is the default format type. + this.formatType = options.credential_source.format?.type || 'text'; + this.formatSubjectTokenFieldName = + options.credential_source.format?.subject_token_field_name; + if (this.formatType !== 'json' && this.formatType !== 'text') { + throw new Error(`Invalid credential_source format "${this.formatType}"`); + } + if (this.formatType === 'json' && !this.formatSubjectTokenFieldName) { + throw new Error( + 'Missing subject_token_field_name for JSON credential_source format' + ); + } + } + + /** + * Triggered when a external subject token is needed to be exchanged for a GCP + * access token via GCP STS endpoint. + * This uses the `options.credential_source` object to figure out how + * to retrieve the token using the current environment. In this case, + * this either retrieves the local credential from a file location (k8s + * workload) or by sending a GET request to a local metadata server (Azure + * workloads). + * @return A promise that resolves with the external subject token. + */ + async retrieveSubjectToken(): Promise { + if (this.file) { + return await this.getTokenFromFile( + this.file!, + this.formatType, + this.formatSubjectTokenFieldName + ); + } + return await this.getTokenFromUrl( + this.url!, + this.formatType, + this.formatSubjectTokenFieldName, + this.headers + ); + } + + /** + * Looks up the external subject token in the file path provided and + * resolves with that token. + * @param file The file path where the external credential is located. + * @param formatType The token file or URL response type (JSON or text). + * @param formatSubjectTokenFieldName For JSON response types, this is the + * subject_token field name. For Azure, this is access_token. For text + * response types, this is ignored. + * @return A promise that resolves with the external subject token. + */ + private async getTokenFromFile( + filePath: string, + formatType: SubjectTokenFormatType, + formatSubjectTokenFieldName?: string + ): Promise { + // 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 = await realpath(filePath); + + if (!(await lstat(filePath)).isFile()) { + throw new Error(); + } + } catch (err) { + err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`; + throw err; + } + + let subjectToken: string | undefined; + const rawText = await readFile(filePath, {encoding: 'utf8'}); + if (formatType === 'text') { + subjectToken = rawText; + } else if (formatType === 'json' && formatSubjectTokenFieldName) { + const json = JSON.parse(rawText) as SubjectTokenJsonResponse; + subjectToken = json[formatSubjectTokenFieldName]; + } + if (!subjectToken) { + throw new Error( + 'Unable to parse the subject_token from the credential_source file' + ); + } + return subjectToken; + } + + /** + * Sends a GET request to the URL provided and resolves with the returned + * external subject token. + * @param url The URL to call to retrieve the subject token. This is typically + * a local metadata server. + * @param formatType The token file or URL response type (JSON or text). + * @param formatSubjectTokenFieldName For JSON response types, this is the + * subject_token field name. For Azure, this is access_token. For text + * response types, this is ignored. + * @param headers The optional additional headers to send with the request to + * the metadata server url. + * @return A promise that resolves with the external subject token. + */ + private async getTokenFromUrl( + url: string, + formatType: SubjectTokenFormatType, + formatSubjectTokenFieldName?: string, + headers?: {[key: string]: string} + ): Promise { + const opts: GaxiosOptions = { + url, + method: 'GET', + headers, + responseType: formatType, + }; + let subjectToken: string | undefined; + if (formatType === 'text') { + const response = await this.transporter.request(opts); + subjectToken = response.data; + } else if (formatType === 'json' && formatSubjectTokenFieldName) { + const response = await this.transporter.request( + opts + ); + subjectToken = response.data[formatSubjectTokenFieldName]; + } + if (!subjectToken) { + throw new Error( + 'Unable to parse the subject_token from the credential_source URL' + ); + } + return subjectToken; + } +} diff --git a/src/auth/oauth2common.ts b/src/auth/oauth2common.ts new file mode 100644 index 00000000..34d1bb6d --- /dev/null +++ b/src/auth/oauth2common.ts @@ -0,0 +1,229 @@ +// Copyright 2021 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. + +import {GaxiosOptions} from 'gaxios'; +import * as querystring from 'querystring'; + +import {Crypto, createCrypto} from '../crypto/crypto'; + +/** List of HTTP methods that accept request bodies. */ +const METHODS_SUPPORTING_REQUEST_BODY = ['PUT', 'POST', 'PATCH']; + +/** + * OAuth error codes. + * https://tools.ietf.org/html/rfc6749#section-5.2 + */ +type OAuthErrorCode = + | 'invalid_request' + | 'invalid_client' + | 'invalid_grant' + | 'unauthorized_client' + | 'unsupported_grant_type' + | 'invalid_scope' + | string; + +/** + * The standard OAuth error response. + * https://tools.ietf.org/html/rfc6749#section-5.2 + */ +export interface OAuthErrorResponse { + error: OAuthErrorCode; + error_description?: string; + error_uri?: string; +} + +/** + * OAuth client authentication types. + * https://tools.ietf.org/html/rfc6749#section-2.3 + */ +export type ConfidentialClientType = 'basic' | 'request-body'; + +/** + * Defines the client authentication credentials for basic and request-body + * credentials. + * https://tools.ietf.org/html/rfc6749#section-2.3.1 + */ +export interface ClientAuthentication { + confidentialClientType: ConfidentialClientType; + clientId: string; + clientSecret?: string; +} + +/** + * Abstract class for handling client authentication in OAuth-based + * operations. + * When request-body client authentication is used, only application/json and + * application/x-www-form-urlencoded content types for HTTP methods that support + * request bodies are supported. + */ +export abstract class OAuthClientAuthHandler { + private crypto: Crypto; + + /** + * Instantiates an OAuth client authentication handler. + * @param clientAuthentication The client auth credentials. + */ + constructor(private readonly clientAuthentication?: ClientAuthentication) { + this.crypto = createCrypto(); + } + + /** + * Applies client authentication on the OAuth request's headers or POST + * body but does not process the request. + * @param opts The GaxiosOptions whose headers or data are to be modified + * depending on the client authentication mechanism to be used. + * @param bearerToken The optional bearer token to use for authentication. + * When this is used, no client authentication credentials are needed. + */ + protected applyClientAuthenticationOptions( + opts: GaxiosOptions, + bearerToken?: string + ) { + // Inject authenticated header. + this.injectAuthenticatedHeaders(opts, bearerToken); + // Inject authenticated request body. + if (!bearerToken) { + this.injectAuthenticatedRequestBody(opts); + } + } + + /** + * Applies client authentication on the request's header if either + * basic authentication or bearer token authentication is selected. + * + * @param opts The GaxiosOptions whose headers or data are to be modified + * depending on the client authentication mechanism to be used. + * @param bearerToken The optional bearer token to use for authentication. + * When this is used, no client authentication credentials are needed. + */ + private injectAuthenticatedHeaders( + opts: GaxiosOptions, + bearerToken?: string + ) { + // Bearer token prioritized higher than basic Auth. + if (bearerToken) { + opts.headers = opts.headers || {}; + Object.assign(opts.headers, { + Authorization: `Bearer ${bearerToken}}`, + }); + } else if (this.clientAuthentication?.confidentialClientType === 'basic') { + opts.headers = opts.headers || {}; + const clientId = this.clientAuthentication!.clientId; + const clientSecret = this.clientAuthentication!.clientSecret || ''; + const base64EncodedCreds = this.crypto.encodeBase64StringUtf8( + `${clientId}:${clientSecret}` + ); + Object.assign(opts.headers, { + Authorization: `Basic ${base64EncodedCreds}`, + }); + } + } + + /** + * Applies client authentication on the request's body if request-body + * client authentication is selected. + * + * @param opts The GaxiosOptions whose headers or data are to be modified + * depending on the client authentication mechanism to be used. + */ + private injectAuthenticatedRequestBody(opts: GaxiosOptions) { + if (this.clientAuthentication?.confidentialClientType === 'request-body') { + const method = (opts.method || 'GET').toUpperCase(); + // Inject authenticated request body. + if (METHODS_SUPPORTING_REQUEST_BODY.indexOf(method) !== -1) { + // Get content-type. + let contentType; + const headers = opts.headers || {}; + for (const key in headers) { + if (key.toLowerCase() === 'content-type' && headers[key]) { + contentType = headers[key].toLowerCase(); + break; + } + } + if (contentType === 'application/x-www-form-urlencoded') { + opts.data = opts.data || ''; + const data = querystring.parse(opts.data); + Object.assign(data, { + client_id: this.clientAuthentication!.clientId, + client_secret: this.clientAuthentication!.clientSecret || '', + }); + opts.data = querystring.stringify(data); + } else if (contentType === 'application/json') { + opts.data = opts.data || {}; + Object.assign(opts.data, { + client_id: this.clientAuthentication!.clientId, + client_secret: this.clientAuthentication!.clientSecret || '', + }); + } else { + throw new Error( + `${contentType} content-types are not supported with ` + + `${this.clientAuthentication!.confidentialClientType} ` + + 'client authentication' + ); + } + } else { + throw new Error( + `${method} HTTP method does not support ` + + `${this.clientAuthentication!.confidentialClientType} ` + + 'client authentication' + ); + } + } + } +} + +/** + * Converts an OAuth error response to a native JavaScript Error. + * @param resp The OAuth error response to convert to a native Error object. + * @param err The optional original error. If provided, the error properties + * will be copied to the new error. + * @return The converted native Error object. + */ +export function getErrorFromOAuthErrorResponse( + resp: OAuthErrorResponse, + err?: Error +): Error { + // Error response. + const errorCode = resp.error; + const errorDescription = resp.error_description; + const errorUri = resp.error_uri; + let message = `Error code ${errorCode}`; + if (typeof errorDescription !== 'undefined') { + message += `: ${errorDescription}`; + } + if (typeof errorUri !== 'undefined') { + message += ` - ${errorUri}`; + } + const newError = new Error(message); + // Copy properties from original error to newly generated error. + if (err) { + const keys = Object.keys(err); + if (err.stack) { + // Copy error.stack if available. + keys.push('stack'); + } + keys.forEach(key => { + // Do not overwrite the message field. + if (key !== 'message') { + Object.defineProperty(newError, key, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: (err! as {[index: string]: any})[key], + writable: false, + enumerable: true, + }); + } + }); + } + return newError; +} diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts new file mode 100644 index 00000000..c86e76cf --- /dev/null +++ b/src/auth/stscredentials.ts @@ -0,0 +1,228 @@ +// Copyright 2021 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. + +import {GaxiosOptions, GaxiosResponse} from 'gaxios'; +import * as querystring from 'querystring'; + +import {DefaultTransporter} from '../transporters'; +import {Headers} from './oauth2client'; +import { + ClientAuthentication, + OAuthClientAuthHandler, + OAuthErrorResponse, + getErrorFromOAuthErrorResponse, +} from './oauth2common'; + +/** + * Defines the interface needed to initialize an StsCredentials instance. + * The interface does not directly map to the spec and instead is converted + * to be compliant with the JavaScript style guide. This is because this is + * instantiated internally. + * StsCredentials implement the OAuth 2.0 token exchange based on + * https://tools.ietf.org/html/rfc8693. + * Request options are defined in + * https://tools.ietf.org/html/rfc8693#section-2.1 + */ +export interface StsCredentialsOptions { + /** + * REQUIRED. The value "urn:ietf:params:oauth:grant-type:token-exchange" + * indicates that a token exchange is being performed. + */ + grantType: string; + /** + * OPTIONAL. A URI that indicates the target service or resource where the + * client intends to use the requested security token. + */ + resource?: string; + /** + * OPTIONAL. The logical name of the target service where the client + * intends to use the requested security token. This serves a purpose + * similar to the "resource" parameter but with the client providing a + * logical name for the target service. + */ + audience?: string; + /** + * OPTIONAL. A list of space-delimited, case-sensitive strings, as defined + * in Section 3.3 of [RFC6749], that allow the client to specify the desired + * scope of the requested security token in the context of the service or + * resource where the token will be used. + */ + scope?: string[]; + /** + * OPTIONAL. An identifier, as described in Section 3 of [RFC8693], eg. + * "urn:ietf:params:oauth:token-type:access_token" for the type of the + * requested security token. + */ + requestedTokenType?: string; + /** + * REQUIRED. A security token that represents the identity of the party on + * behalf of whom the request is being made. + */ + subjectToken: string; + /** + * REQUIRED. An identifier, as described in Section 3 of [RFC8693], that + * indicates the type of the security token in the "subject_token" parameter. + */ + subjectTokenType: string; + actingParty?: { + /** + * OPTIONAL. A security token that represents the identity of the acting + * party. Typically, this will be the party that is authorized to use the + * requested security token and act on behalf of the subject. + */ + actorToken: string; + /** + * An identifier, as described in Section 3, that indicates the type of the + * security token in the "actor_token" parameter. This is REQUIRED when the + * "actor_token" parameter is present in the request but MUST NOT be + * included otherwise. + */ + actorTokenType: string; + }; +} + +/** + * Defines the standard request options as defined by the OAuth token + * exchange spec: https://tools.ietf.org/html/rfc8693#section-2.1 + */ +interface StsRequestOptions { + grant_type: string; + resource?: string; + audience?: string; + scope?: string; + requested_token_type?: string; + subject_token: string; + subject_token_type: string; + actor_token?: string; + actor_token_type?: string; + client_id?: string; + client_secret?: string; + // GCP-specific non-standard field. + options?: string; +} + +/** + * Defines the OAuth 2.0 token exchange successful response based on + * https://tools.ietf.org/html/rfc8693#section-2.2.1 + */ +export interface StsSuccessfulResponse { + access_token: string; + issued_token_type: string; + token_type: string; + expires_in: number; + refresh_token?: string; + scope: string; + res?: GaxiosResponse | null; +} + +/** + * Implements the OAuth 2.0 token exchange based on + * https://tools.ietf.org/html/rfc8693 + */ +export class StsCredentials extends OAuthClientAuthHandler { + private transporter: DefaultTransporter; + + /** + * Initializes an STS credentials instance. + * @param tokenExchangeEndpoint The token exchange endpoint. + * @param clientAuthentication The client authentication credentials if + * available. + */ + constructor( + private readonly tokenExchangeEndpoint: string, + clientAuthentication?: ClientAuthentication + ) { + super(clientAuthentication); + this.transporter = new DefaultTransporter(); + } + + /** + * Exchanges the provided token for another type of token based on the + * rfc8693 spec. + * @param stsCredentialsOptions The token exchange options used to populate + * the token exchange request. + * @param additionalHeaders Optional additional headers to pass along the + * request. + * @param options Optional additional GCP-specific non-spec defined options + * to send with the request. + * Example: `&options=${encodeUriComponent(JSON.stringified(options))}` + * @return A promise that resolves with the token exchange response containing + * the requested token and its expiration time. + */ + async exchangeToken( + stsCredentialsOptions: StsCredentialsOptions, + additionalHeaders?: Headers, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options?: {[key: string]: any} + ): Promise { + const values: StsRequestOptions = { + grant_type: stsCredentialsOptions.grantType, + resource: stsCredentialsOptions.resource, + audience: stsCredentialsOptions.audience, + scope: stsCredentialsOptions.scope?.join(' '), + requested_token_type: stsCredentialsOptions.requestedTokenType, + subject_token: stsCredentialsOptions.subjectToken, + subject_token_type: stsCredentialsOptions.subjectTokenType, + actor_token: stsCredentialsOptions.actingParty?.actorToken, + actor_token_type: stsCredentialsOptions.actingParty?.actorTokenType, + // Non-standard GCP-specific options. + options: options && JSON.stringify(options), + }; + // Remove undefined fields. + Object.keys(values).forEach(key => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (typeof (values as {[index: string]: any})[key] === 'undefined') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (values as {[index: string]: any})[key]; + } + }); + + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + // Inject additional STS headers if available. + Object.assign(headers, additionalHeaders || {}); + + const opts: GaxiosOptions = { + url: this.tokenExchangeEndpoint, + method: 'POST', + headers, + data: querystring.stringify(values), + responseType: 'json', + }; + // Apply OAuth client authentication. + this.applyClientAuthenticationOptions(opts); + + try { + const response = await this.transporter.request( + opts + ); + // Successful response. + const stsSuccessfulResponse = response.data; + stsSuccessfulResponse.res = response; + return stsSuccessfulResponse; + } catch (error) { + // Translate error to OAuthError. + if (error.response) { + throw getErrorFromOAuthErrorResponse( + error.response.data as OAuthErrorResponse, + // Preserve other fields from the original error. + error + ); + } + // Request could fail before the server responds. + throw error; + } + } +} diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index 5e1665f0..feba104a 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -26,7 +26,7 @@ if (typeof process === 'undefined' && typeof TextEncoder === 'undefined') { require('fast-text-encoding'); } -import {Crypto, JwkCertificate} from '../crypto'; +import {Crypto, JwkCertificate, fromArrayBufferToHex} from '../crypto'; export class BrowserCrypto implements Crypto { constructor() { @@ -140,4 +140,63 @@ export class BrowserCrypto implements Crypto { const result = base64js.fromByteArray(uint8array); return result; } + + /** + * Computes the SHA-256 hash of the provided string. + * @param str The plain text string to hash. + * @return A promise that resolves with the SHA-256 hash of the provided + * string in hexadecimal encoding. + */ + async sha256DigestHex(str: string): Promise { + // SubtleCrypto digest() method is async, so we must make + // this method async as well. + + // To calculate SHA256 digest using SubtleCrypto, we first + // need to convert an input string to an ArrayBuffer: + // eslint-disable-next-line node/no-unsupported-features/node-builtins + const inputBuffer = new TextEncoder().encode(str); + + // Result is ArrayBuffer as well. + const outputBuffer = await window.crypto.subtle.digest( + 'SHA-256', + inputBuffer + ); + + return fromArrayBufferToHex(outputBuffer); + } + + /** + * Computes the HMAC hash of a message using the provided crypto key and the + * SHA-256 algorithm. + * @param key The secret crypto key in utf-8 or ArrayBuffer format. + * @param msg The plain text message. + * @return A promise that resolves with the HMAC-SHA256 hash in ArrayBuffer + * format. + */ + async signWithHmacSha256( + key: string | ArrayBuffer, + msg: string + ): Promise { + // Convert key, if provided in ArrayBuffer format, to string. + const rawKey = + typeof key === 'string' + ? key + : String.fromCharCode(...new Uint16Array(key)); + + // eslint-disable-next-line node/no-unsupported-features/node-builtins + const enc = new TextEncoder(); + const cryptoKey = await window.crypto.subtle.importKey( + 'raw', + enc.encode(rawKey), + { + name: 'HMAC', + hash: { + name: 'SHA-256', + }, + }, + false, + ['sign'] + ); + return window.crypto.subtle.sign('HMAC', cryptoKey, enc.encode(msg)); + } } diff --git a/src/crypto/crypto.ts b/src/crypto/crypto.ts index 27ce5a9d..be50295e 100644 --- a/src/crypto/crypto.ts +++ b/src/crypto/crypto.ts @@ -51,6 +51,26 @@ export interface Crypto { ): Promise; decodeBase64StringUtf8(base64: string): string; encodeBase64StringUtf8(text: string): string; + /** + * Computes the SHA-256 hash of the provided string. + * @param str The plain text string to hash. + * @return A promise that resolves with the SHA-256 hash of the provided + * string in hexadecimal encoding. + */ + sha256DigestHex(str: string): Promise; + + /** + * Computes the HMAC hash of a message using the provided crypto key and the + * SHA-256 algorithm. + * @param key The secret crypto key in utf-8 or ArrayBuffer format. + * @param msg The plain text message. + * @return A promise that resolves with the HMAC-SHA256 hash in ArrayBuffer + * format. + */ + signWithHmacSha256( + key: string | ArrayBuffer, + msg: string + ): Promise; } export function createCrypto(): Crypto { @@ -67,3 +87,19 @@ export function hasBrowserCrypto() { typeof window.crypto.subtle !== 'undefined' ); } + +/** + * Converts an ArrayBuffer to a hexadecimal string. + * @param arrayBuffer The ArrayBuffer to convert to hexadecimal string. + * @return The hexadecimal encoding of the ArrayBuffer. + */ +export function fromArrayBufferToHex(arrayBuffer: ArrayBuffer): string { + // Convert buffer to byte array. + const byteArray = Array.from(new Uint8Array(arrayBuffer)); + // Convert bytes to hex string. + return byteArray + .map(byte => { + return byte.toString(16).padStart(2, '0'); + }) + .join(''); +} diff --git a/src/crypto/node/crypto.ts b/src/crypto/node/crypto.ts index 58b60671..eaaff400 100644 --- a/src/crypto/node/crypto.ts +++ b/src/crypto/node/crypto.ts @@ -49,4 +49,54 @@ export class NodeCrypto implements Crypto { encodeBase64StringUtf8(text: string): string { return Buffer.from(text, 'utf-8').toString('base64'); } + + /** + * Computes the SHA-256 hash of the provided string. + * @param str The plain text string to hash. + * @return A promise that resolves with the SHA-256 hash of the provided + * string in hexadecimal encoding. + */ + async sha256DigestHex(str: string): Promise { + return crypto.createHash('sha256').update(str).digest('hex'); + } + + /** + * Computes the HMAC hash of a message using the provided crypto key and the + * SHA-256 algorithm. + * @param key The secret crypto key in utf-8 or ArrayBuffer format. + * @param msg The plain text message. + * @return A promise that resolves with the HMAC-SHA256 hash in ArrayBuffer + * format. + */ + async signWithHmacSha256( + key: string | ArrayBuffer, + msg: string + ): Promise { + const cryptoKey = typeof key === 'string' ? key : toBuffer(key); + return toArrayBuffer( + crypto.createHmac('sha256', cryptoKey).update(msg).digest() + ); + } +} + +/** + * Converts a Node.js Buffer to an ArrayBuffer. + * https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer + * @param buffer The Buffer input to covert. + * @return The ArrayBuffer representation of the input. + */ +function toArrayBuffer(buffer: Buffer): ArrayBuffer { + return buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ); +} + +/** + * Converts an ArrayBuffer to a Node.js Buffer. + * @param arrayBuffer The ArrayBuffer input to covert. + * @return The Buffer representation of the input. + */ +function toBuffer(arrayBuffer: ArrayBuffer): Buffer { + return Buffer.from(arrayBuffer); } diff --git a/src/index.ts b/src/index.ts index 722e14a4..6e2f23b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,15 @@ export { UserRefreshClient, UserRefreshClientOptions, } from './auth/refreshclient'; +export {AwsClient, AwsClientOptions} from './auth/awsclient'; +export { + IdentityPoolClient, + IdentityPoolClientOptions, +} from './auth/identitypoolclient'; +export { + ExternalAccountClient, + ExternalAccountClientOptions, +} from './auth/externalclient'; export {DefaultTransporter} from './transporters'; const auth = new GoogleAuth(); diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts new file mode 100644 index 00000000..391616e2 --- /dev/null +++ b/test/externalclienthelper.ts @@ -0,0 +1,141 @@ +// Copyright 2021 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. + +import * as assert from 'assert'; +import * as nock from 'nock'; +import * as qs from 'querystring'; +import {GetAccessTokenResponse} from '../src/auth/oauth2client'; +import {OAuthErrorResponse} from '../src/auth/oauth2common'; +import {StsSuccessfulResponse} from '../src/auth/stscredentials'; +import { + IamGenerateAccessTokenResponse, + ProjectInfo, +} from '../src/auth/baseexternalclient'; + +interface CloudRequestError { + error: { + code: number; + message: string; + status: string; + }; +} + +interface NockMockStsToken { + statusCode: number; + response: StsSuccessfulResponse | OAuthErrorResponse; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: {[key: string]: any}; + additionalHeaders?: {[key: string]: string}; +} + +interface NockMockGenerateAccessToken { + statusCode: number; + token: string; + response: IamGenerateAccessTokenResponse | CloudRequestError; + scopes: string[]; +} + +const defaultProjectNumber = '123456'; +const poolId = 'POOL_ID'; +const providerId = 'PROVIDER_ID'; +const baseUrl = 'https://sts.googleapis.com'; +const path = '/v1/token'; +const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; +const saBaseUrl = 'https://iamcredentials.googleapis.com'; +const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; + +export function mockStsTokenExchange( + nockParams: NockMockStsToken[] +): nock.Scope { + const scope = nock(baseUrl); + nockParams.forEach(nockMockStsToken => { + const headers = Object.assign( + { + 'content-type': 'application/x-www-form-urlencoded', + }, + nockMockStsToken.additionalHeaders || {} + ); + scope + .post(path, qs.stringify(nockMockStsToken.request), { + reqheaders: headers, + }) + .reply(nockMockStsToken.statusCode, nockMockStsToken.response); + }); + return scope; +} + +export function mockGenerateAccessToken( + nockParams: NockMockGenerateAccessToken[] +): nock.Scope { + const scope = nock(saBaseUrl); + nockParams.forEach(nockMockGenerateAccessToken => { + const token = nockMockGenerateAccessToken.token; + scope + .post( + saPath, + { + scope: nockMockGenerateAccessToken.scopes, + }, + { + reqheaders: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + ) + .reply( + nockMockGenerateAccessToken.statusCode, + nockMockGenerateAccessToken.response + ); + }); + return scope; +} + +export function getAudience( + projectNumber: string = defaultProjectNumber +): string { + return ( + `//iam.googleapis.com/projects/${projectNumber}` + + `/locations/global/workloadIdentityPools/${poolId}/` + + `providers/${providerId}` + ); +} + +export function getTokenUrl(): string { + return `${baseUrl}${path}`; +} + +export function getServiceAccountImpersonationUrl(): string { + return `${saBaseUrl}${saPath}`; +} + +export function assertGaxiosResponsePresent(resp: GetAccessTokenResponse) { + const gaxiosResponse = resp.res || {}; + assert('data' in gaxiosResponse && 'status' in gaxiosResponse); +} + +export function mockCloudResourceManager( + projectNumber: string, + accessToken: string, + statusCode: number, + response: ProjectInfo | CloudRequestError +): nock.Scope { + return nock('https://cloudresourcemanager.googleapis.com') + .get(`/v1/projects/${projectNumber}`, undefined, { + reqheaders: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .reply(statusCode, response); +} diff --git a/test/fixtures/aws-security-credentials-fake.json b/test/fixtures/aws-security-credentials-fake.json new file mode 100644 index 00000000..8bd8ca85 --- /dev/null +++ b/test/fixtures/aws-security-credentials-fake.json @@ -0,0 +1,9 @@ +{ + "Code" : "Success", + "LastUpdated" : "2020-08-11T19:33:07Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ASIARD4OQDT6A77FR3CL", + "SecretAccessKey" : "Y8AfSaucF37G4PpvfguKZ3/l7Id4uocLXxX0+VTx", + "Token" : "IQoJb3JpZ2luX2VjEIz//////////wEaCXVzLWVhc3QtMiJGMEQCIH7MHX/Oy/OB8OlLQa9GrqU1B914+iMikqWQW7vPCKlgAiA/Lsv8Jcafn14owfxXn95FURZNKaaphj0ykpmS+Ki+CSq0AwhlEAAaDDA3NzA3MTM5MTk5NiIMx9sAeP1ovlMTMKLjKpEDwuJQg41/QUKx0laTZYjPlQvjwSqS3OB9P1KAXPWSLkliVMMqaHqelvMF/WO/glv3KwuTfQsavRNs3v5pcSEm4SPO3l7mCs7KrQUHwGP0neZhIKxEXy+Ls//1C/Bqt53NL+LSbaGv6RPHaX82laz2qElphg95aVLdYgIFY6JWV5fzyjgnhz0DQmy62/Vi8pNcM2/VnxeCQ8CC8dRDSt52ry2v+nc77vstuI9xV5k8mPtnaPoJDRANh0bjwY5Sdwkbp+mGRUJBAQRlNgHUJusefXQgVKBCiyJY4w3Csd8Bgj9IyDV+Azuy1jQqfFZWgP68LSz5bURyIjlWDQunO82stZ0BgplKKAa/KJHBPCp8Qi6i99uy7qh76FQAqgVTsnDuU6fGpHDcsDSGoCls2HgZjZFPeOj8mmRhFk1Xqvkbjuz8V1cJk54d3gIJvQt8gD2D6yJQZecnuGWd5K2e2HohvCc8Fc9kBl1300nUJPV+k4tr/A5R/0QfEKOZL1/k5lf1g9CREnrM8LVkGxCgdYMxLQow1uTL+QU67AHRRSp5PhhGX4Rek+01vdYSnJCMaPhSEgcLqDlQkhk6MPsyT91QMXcWmyO+cAZwUPwnRamFepuP4K8k2KVXs/LIJHLELwAZ0ekyaS7CptgOqS7uaSTFG3U+vzFZLEnGvWQ7y9IPNQZ+Dffgh4p3vF4J68y9049sI6Sr5d5wbKkcbm8hdCDHZcv4lnqohquPirLiFQ3q7B17V9krMPu3mz1cg4Ekgcrn/E09NTsxAqD8NcZ7C7ECom9r+X3zkDOxaajW6hu3Az8hGlyylDaMiFfRbBJpTIlxp7jfa7CxikNgNtEKLH9iCzvuSg2vhA==", + "Expiration" : "2020-08-11T07:35:49Z" +} diff --git a/test/fixtures/external-account-cred.json b/test/fixtures/external-account-cred.json new file mode 100644 index 00000000..9e7d029b --- /dev/null +++ b/test/fixtures/external-account-cred.json @@ -0,0 +1,9 @@ +{ + "type": "external_account", + "audience": "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + "token_url": "https://sts.googleapis.com/v1/token", + "credential_source": { + "file": "./test/fixtures/external-subject-token.txt" + } +} diff --git a/test/fixtures/external-subject-token.json b/test/fixtures/external-subject-token.json new file mode 100644 index 00000000..6bba08dc --- /dev/null +++ b/test/fixtures/external-subject-token.json @@ -0,0 +1,3 @@ +{ + "access_token": "HEADER.SIMULATED_JWT_PAYLOAD.SIGNATURE" +} diff --git a/test/fixtures/external-subject-token.txt b/test/fixtures/external-subject-token.txt new file mode 100644 index 00000000..c668d8f7 --- /dev/null +++ b/test/fixtures/external-subject-token.txt @@ -0,0 +1 @@ +HEADER.SIMULATED_JWT_PAYLOAD.SIGNATURE \ No newline at end of file diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts new file mode 100644 index 00000000..b318cea2 --- /dev/null +++ b/test/test.awsclient.ts @@ -0,0 +1,712 @@ +// Copyright 2021 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. + +import * as assert from 'assert'; +import {describe, it, afterEach, beforeEach} from 'mocha'; +import * as nock from 'nock'; +import * as sinon from 'sinon'; +import {AwsClient} from '../src/auth/awsclient'; +import {StsSuccessfulResponse} from '../src/auth/stscredentials'; +import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; +import { + assertGaxiosResponsePresent, + getAudience, + getTokenUrl, + getServiceAccountImpersonationUrl, + mockGenerateAccessToken, + mockStsTokenExchange, +} from './externalclienthelper'; + +nock.disableNetConnect(); + +const ONE_HOUR_IN_SECS = 3600; + +describe('AwsClient', () => { + let clock: sinon.SinonFakeTimers; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const awsSecurityCredentials = require('../../test/fixtures/aws-security-credentials-fake.json'); + const referenceDate = new Date('2020-08-11T06:55:22.345Z'); + const amzDate = '20200811T065522Z'; + const dateStamp = '20200811'; + const awsRegion = 'us-east-2'; + const accessKeyId = awsSecurityCredentials.AccessKeyId; + const secretAccessKey = awsSecurityCredentials.SecretAccessKey; + const token = awsSecurityCredentials.Token; + const awsRole = 'gcp-aws-role'; + const audience = getAudience(); + const metadataBaseUrl = 'http://169.254.169.254'; + const awsCredentialSource = { + environment_id: 'aws1', + region_url: `${metadataBaseUrl}/latest/meta-data/placement/availability-zone`, + url: `${metadataBaseUrl}/latest/meta-data/iam/security-credentials`, + regional_cred_verification_url: + 'https://sts.{region}.amazonaws.com?' + + 'Action=GetCallerIdentity&Version=2011-06-15', + }; + const awsOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: awsCredentialSource, + }; + const awsOptionsWithSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + awsOptions + ); + const stsSuccessfulResponse: StsSuccessfulResponse = { + access_token: 'ACCESS_TOKEN', + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: ONE_HOUR_IN_SECS, + scope: 'scope1 scope2', + }; + // Signature retrieved from "signed POST request" test in test.awsclient.ts. + const expectedSignedRequest = { + url: + 'https://sts.us-east-2.amazonaws.com' + + '?Action=GetCallerIdentity&Version=2011-06-15', + method: 'POST', + headers: { + Authorization: + `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + + `${dateStamp}/${awsRegion}/sts/aws4_request, SignedHeaders=host;` + + 'x-amz-date;x-amz-security-token, Signature=' + + '73452984e4a880ffdc5c392355733ec3f5ba310d5e0609a89244440cadfe7a7a', + host: 'sts.us-east-2.amazonaws.com', + 'x-amz-date': amzDate, + 'x-amz-security-token': token, + }, + }; + const expectedSubjectToken = encodeURIComponent( + JSON.stringify({ + url: expectedSignedRequest.url, + method: expectedSignedRequest.method, + headers: [ + { + key: 'x-goog-cloud-target-resource', + value: awsOptions.audience, + }, + { + key: 'x-amz-date', + value: expectedSignedRequest.headers['x-amz-date'], + }, + { + key: 'Authorization', + value: expectedSignedRequest.headers.Authorization, + }, + { + key: 'host', + value: expectedSignedRequest.headers.host, + }, + { + key: 'x-amz-security-token', + value: expectedSignedRequest.headers['x-amz-security-token'], + }, + ], + }) + ); + // Signature retrieved from "signed request when AWS credentials have no + // token" test in test.awsclient.ts. + const expectedSignedRequestNoToken = { + url: + 'https://sts.us-east-2.amazonaws.com' + + '?Action=GetCallerIdentity&Version=2011-06-15', + method: 'POST', + headers: { + Authorization: + `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + + `${dateStamp}/${awsRegion}/sts/aws4_request, SignedHeaders=host;` + + 'x-amz-date, Signature=' + + 'd095ba304919cd0d5570ba8a3787884ee78b860f268ed040ba23831d55536d56', + host: 'sts.us-east-2.amazonaws.com', + 'x-amz-date': amzDate, + }, + }; + const expectedSubjectTokenNoToken = encodeURIComponent( + JSON.stringify({ + url: expectedSignedRequestNoToken.url, + method: expectedSignedRequestNoToken.method, + headers: [ + { + key: 'x-goog-cloud-target-resource', + value: awsOptions.audience, + }, + { + key: 'x-amz-date', + value: expectedSignedRequestNoToken.headers['x-amz-date'], + }, + { + key: 'Authorization', + value: expectedSignedRequestNoToken.headers.Authorization, + }, + { + key: 'host', + value: expectedSignedRequestNoToken.headers.host, + }, + ], + }) + ); + + beforeEach(() => { + clock = sinon.useFakeTimers(referenceDate); + }); + + afterEach(() => { + if (clock) { + clock.restore(); + } + }); + + it('should be a subclass of ExternalAccountClient', () => { + assert(AwsClient.prototype instanceof BaseExternalAccountClient); + }); + + describe('Constructor', () => { + const requiredCredentialSourceFields = [ + 'environment_id', + 'regional_cred_verification_url', + ]; + requiredCredentialSourceFields.forEach(required => { + it(`should throw when credential_source is missing ${required}`, () => { + const expectedError = new Error( + 'No valid AWS "credential_source" provided' + ); + const invalidCredentialSource = Object.assign({}, awsCredentialSource); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (invalidCredentialSource as any)[required]; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + }); + + it('should throw when an unsupported environment ID is provided', () => { + const expectedError = new Error( + 'No valid AWS "credential_source" provided' + ); + const invalidCredentialSource = Object.assign({}, awsCredentialSource); + invalidCredentialSource.environment_id = 'azure1'; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + + it('should throw when an unsupported environment version is provided', () => { + const expectedError = new Error( + 'aws version "3" is not supported in the current build.' + ); + const invalidCredentialSource = Object.assign({}, awsCredentialSource); + invalidCredentialSource.environment_id = 'aws3'; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + + it('should not throw when valid AWS options are provided', () => { + assert.doesNotThrow(() => { + return new AwsClient(awsOptions); + }); + }); + }); + + describe('for security_credentials retrieved tokens', () => { + describe('retrieveSubjectToken()', () => { + it('should resolve on success', async () => { + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials); + + const client = new AwsClient(awsOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scope.done(); + }); + + it('should resolve on success with permanent creds', async () => { + const permanentAwsSecurityCredentials = Object.assign( + {}, + awsSecurityCredentials + ); + delete permanentAwsSecurityCredentials.Token; + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, permanentAwsSecurityCredentials); + + const client = new AwsClient(awsOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + scope.done(); + }); + + it('should re-calculate role name on successive calls', async () => { + const otherRole = 'some-other-role'; + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, otherRole) + .get(`/latest/meta-data/iam/security-credentials/${otherRole}`) + .reply(200, awsSecurityCredentials); + + const client = new AwsClient(awsOptions); + const subjectToken = await client.retrieveSubjectToken(); + const subjectToken2 = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + assert.deepEqual(subjectToken2, expectedSubjectToken); + scope.done(); + }); + + it('should reject when AWS region is not determined', async () => { + // Simulate error during region retrieval. + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(500); + + const client = new AwsClient(awsOptions); + + await assert.rejects(client.retrieveSubjectToken(), { + code: '500', + }); + scope.done(); + }); + + it('should reject when AWS role name is not determined', async () => { + // Simulate error during region retrieval. + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(403); + + const client = new AwsClient(awsOptions); + + await assert.rejects(client.retrieveSubjectToken(), { + code: '403', + }); + scope.done(); + }); + + it('should reject when AWS security creds are not found', async () => { + // Simulate error during security credentials retrieval. + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(408); + + const client = new AwsClient(awsOptions); + + await assert.rejects(client.retrieveSubjectToken(), { + code: '408', + }); + scope.done(); + }); + + it('should reject when "credential_source.url" is missing', async () => { + const expectedError = new Error( + 'Unable to determine AWS role name due to missing ' + + '"options.credential_source.url"' + ); + const missingUrlCredentialSource = Object.assign( + {}, + awsCredentialSource + ); + delete missingUrlCredentialSource.url; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: missingUrlCredentialSource, + }; + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`); + + const client = new AwsClient(invalidOptions); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + scope.done(); + }); + + it('should reject when "credential_source.region_url" is missing', async () => { + const expectedError = new Error( + 'Unable to determine AWS region due to missing ' + + '"options.credential_source.region_url"' + ); + const missingRegionUrlCredentialSource = Object.assign( + {}, + awsCredentialSource + ); + delete missingRegionUrlCredentialSource.region_url; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: missingRegionUrlCredentialSource, + }; + + const client = new AwsClient(invalidOptions); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + }); + + describe('getAccessToken()', () => { + it('should resolve on retrieveSubjectToken success', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: expectedSubjectToken, + subject_token_type: + 'urn:ietf:params:aws:token-type:aws4_request', + }, + }, + ]) + ); + scopes.push( + nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials) + ); + + const client = new AwsClient(awsOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should handle service account access token', async () => { + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date( + referenceDate.getTime() + ONE_HOUR_IN_SECS * 1000 + ).toISOString(), + }; + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials), + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: expectedSubjectToken, + subject_token_type: + 'urn:ietf:params:aws:token-type:aws4_request', + }, + }, + ]), + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new AwsClient(awsOptionsWithSA); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should reject on retrieveSubjectToken error', async () => { + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(500); + + const client = new AwsClient(awsOptions); + + await assert.rejects(client.getAccessToken(), { + code: '500', + }); + scope.done(); + }); + }); + }); + + describe('for environment variables retrieved tokens', () => { + let envAwsAccessKeyId: string | undefined; + let envAwsSecretAccessKey: string | undefined; + let envAwsSessionToken: string | undefined; + let envAwsRegion: string | undefined; + + beforeEach(() => { + // Store external state. + envAwsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; + envAwsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; + envAwsSessionToken = process.env.AWS_SESSION_TOKEN; + envAwsAccessKeyId = process.env.AWS_REGION; + // Reset environment variables. + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + delete process.env.AWS_SESSION_TOKEN; + delete process.env.AWS_REGION; + }); + + afterEach(() => { + // Restore environment variables. + if (envAwsAccessKeyId) { + process.env.AWS_ACCESS_KEY_ID = envAwsAccessKeyId; + } else { + delete process.env.AWS_ACCESS_KEY_ID; + } + if (envAwsSecretAccessKey) { + process.env.AWS_SECRET_ACCESS_KEY = envAwsSecretAccessKey; + } else { + delete process.env.AWS_SECRET_ACCESS_KEY; + } + if (envAwsSessionToken) { + process.env.AWS_SESSION_TOKEN = envAwsSessionToken; + } else { + delete process.env.AWS_SESSION_TOKEN; + } + if (envAwsRegion) { + process.env.AWS_REGION = envAwsRegion; + } else { + delete process.env.AWS_REGION; + } + }); + + describe('retrieveSubjectToken()', () => { + it('should resolve on success for permanent creds', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`); + + const client = new AwsClient(awsOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + scope.done(); + }); + + it('should resolve on success for temporary creds', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + process.env.AWS_SESSION_TOKEN = token; + + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`); + + const client = new AwsClient(awsOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scope.done(); + }); + + it('should reject when AWS region is not determined', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + + // Simulate error during region retrieval. + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(500); + + const client = new AwsClient(awsOptions); + + await assert.rejects(client.retrieveSubjectToken(), { + code: '500', + }); + scope.done(); + }); + + it('should resolve when AWS region is set as environment variable', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + process.env.AWS_REGION = awsRegion; + + const client = new AwsClient(awsOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + }); + + it('should resolve without optional credentials_source fields', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + process.env.AWS_REGION = awsRegion; + const requiredOnlyCredentialSource = Object.assign( + {}, + awsCredentialSource + ); + // Remove all optional fields. + delete requiredOnlyCredentialSource.region_url; + delete requiredOnlyCredentialSource.url; + const requiredOnlyOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: requiredOnlyCredentialSource, + }; + + const client = new AwsClient(requiredOnlyOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + }); + }); + + describe('getAccessToken()', () => { + it('should resolve on retrieveSubjectToken success', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: expectedSubjectTokenNoToken, + subject_token_type: + 'urn:ietf:params:aws:token-type:aws4_request', + }, + }, + ]) + ); + scopes.push( + nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + ); + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + + const client = new AwsClient(awsOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should reject on retrieveSubjectToken error', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(500); + + const client = new AwsClient(awsOptions); + + await assert.rejects(client.getAccessToken(), { + code: '500', + }); + scope.done(); + }); + }); + }); +}); diff --git a/test/test.awsrequestsigner.ts b/test/test.awsrequestsigner.ts new file mode 100644 index 00000000..ebe3824e --- /dev/null +++ b/test/test.awsrequestsigner.ts @@ -0,0 +1,742 @@ +// Copyright 2021 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. + +import * as assert from 'assert'; +import {describe, it, afterEach, beforeEach} from 'mocha'; +import * as sinon from 'sinon'; +import {AwsRequestSigner} from '../src/auth/awsrequestsigner'; +import {GaxiosOptions} from 'gaxios'; + +/** Defines the interface to facilitate testing of AWS request signing. */ +interface AwsRequestSignerTest { + // Test description. + description: string; + // The mock time when the signature is generated. + referenceDate: Date; + // AWS request signer instance. + instance: AwsRequestSigner; + // The raw input request. + originalRequest: GaxiosOptions; + // The expected signed output request. + getSignedRequest: () => GaxiosOptions; +} + +describe('AwsRequestSigner', () => { + let clock: sinon.SinonFakeTimers; + // Load AWS credentials from a sample security_credentials response. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const awsSecurityCredentials = require('../../test/fixtures/aws-security-credentials-fake.json'); + const accessKeyId = awsSecurityCredentials.AccessKeyId; + const secretAccessKey = awsSecurityCredentials.SecretAccessKey; + const token = awsSecurityCredentials.Token; + + beforeEach(() => { + clock = sinon.useFakeTimers(0); + }); + + afterEach(() => { + if (clock) { + clock.restore(); + } + }); + + describe('getRequestOptions()', () => { + const awsError = new Error('Error retrieving AWS security credentials'); + // Successful AWS credentials retrieval. + // In this case, temporary credentials are returned. + const getCredentials = async () => { + return { + accessKeyId, + secretAccessKey, + token, + }; + }; + // Successful AWS credentials retrieval. + // In this case, permanent credentials are returned (no session token). + const getCredentialsWithoutToken = async () => { + return { + accessKeyId, + secretAccessKey, + }; + }; + // Failing AWS credentials retrieval. + const getCredentialsUnsuccessful = async () => { + throw awsError; + }; + // Sample request parameters. + const requestParams = { + KeySchema: [ + { + KeyType: 'HASH', + AttributeName: 'Id', + }, + ], + TableName: 'TestTable', + AttributeDefinitions: [ + { + AttributeName: 'Id', + AttributeType: 'S', + }, + ], + ProvisionedThroughput: { + WriteCapacityUnits: 5, + ReadCapacityUnits: 5, + }, + }; + // List of various requests and their expected signatures. + // Examples source: + // https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html + const getRequestOptionsTests: AwsRequestSignerTest[] = [ + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.sreq + description: 'signed GET request (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'GET', + url: 'https://host.foo.com', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }, + getSignedRequest: () => { + const signature = + 'b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470'; + return { + url: 'https://host.foo.com', + method: 'GET', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.sreq + description: + 'signed GET request with relative path (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'GET', + url: 'https://host.foo.com/foo/bar/../..', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }, + getSignedRequest: () => { + const signature = + 'b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470'; + return { + url: 'https://host.foo.com/foo/bar/../..', + method: 'GET', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.sreq + description: 'signed GET request with /./ path (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'GET', + url: 'https://host.foo.com/./', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }, + getSignedRequest: () => { + const signature = + 'b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470'; + return { + url: 'https://host.foo.com/./', + method: 'GET', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.sreq + description: + 'signed GET request with pointless dot path (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'GET', + url: 'https://host.foo.com/./foo', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }, + getSignedRequest: () => { + const signature = + '910e4d6c9abafaf87898e1eb4c929135782ea25bb0279703146455745391e63a'; + return { + url: 'https://host.foo.com/./foo', + method: 'GET', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.sreq + description: 'signed GET request with utf8 path (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'GET', + url: 'https://host.foo.com/%E1%88%B4', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }, + getSignedRequest: () => { + const signature = + '8d6634c189aa8c75c2e51e106b6b5121bed103fdb351f7d7d4381c738823af74'; + return { + url: 'https://host.foo.com/%E1%88%B4', + method: 'GET', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.sreq + description: + 'signed GET request with uplicate query key (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'GET', + url: 'https://host.foo.com/?foo=Zoo&foo=aha', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }, + getSignedRequest: () => { + const signature = + 'be7148d34ebccdc6423b19085378aa0bee970bdc61d144bd1a8c48c33079ab09'; + return { + url: 'https://host.foo.com/?foo=Zoo&foo=aha', + method: 'GET', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.sreq + description: 'signed GET request with utf8 query (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'GET', + url: 'https://host.foo.com/?ሴ=bar', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }, + getSignedRequest: () => { + const signature = + '6fb359e9a05394cc7074e0feb42573a2601abc0c869a953e8c5c12e4e01f1a8c'; + return { + url: 'https://host.foo.com/?ሴ=bar', + method: 'GET', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.sreq + description: + 'signed POST request with sorted headers (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'POST', + url: 'https://host.foo.com/', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + ZOO: 'zoobar', + }, + }, + getSignedRequest: () => { + const signature = + 'b7a95a52518abbca0964a999a880429ab734f35ebbf1235bd79a5de87756dc4a'; + return { + url: 'https://host.foo.com/', + method: 'POST', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host;zoo, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + ZOO: 'zoobar', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.sreq + description: + 'signed POST request with upper case header value from ' + + 'AWS Python test harness', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'POST', + url: 'https://host.foo.com/', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + zoo: 'ZOOBAR', + }, + }, + getSignedRequest: () => { + const signature = + '273313af9d0c265c531e11db70bbd653f3ba074c1009239e8559d3987039cad7'; + return { + url: 'https://host.foo.com/', + method: 'POST', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host;zoo, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + zoo: 'ZOOBAR', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.sreq + description: + 'signed POST request with header and no body (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'POST', + url: 'https://host.foo.com', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + p: 'phfft', + }, + }, + getSignedRequest: () => { + const signature = + 'debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592'; + return { + url: 'https://host.foo.com', + method: 'POST', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host;p, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + p: 'phfft', + }, + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.sreq + description: + 'signed POST request with body and no header (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'POST', + url: 'https://host.foo.com', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + body: 'foo=bar', + }, + getSignedRequest: () => { + const signature = + '5a15b22cf462f047318703b92e6f4f38884e4a7ab7b1d6426ca46a8bd1c26cbc'; + return { + url: 'https://host.foo.com', + method: 'POST', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + 'aws4_request, SignedHeaders=content-type;date;host, ' + + `Signature=${signature}`, + host: 'host.foo.com', + 'Content-Type': 'application/x-www-form-urlencoded', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + body: 'foo=bar', + }; + }, + }, + { + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.req + // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.sreq + description: + 'signed POST request with querystring (AWS botocore tests)', + referenceDate: new Date('2011-09-09T23:36:00.000Z'), + instance: new AwsRequestSigner(async () => { + return { + accessKeyId: 'AKIDEXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', + }; + }, 'us-east-1'), + originalRequest: { + method: 'POST', + url: 'https://host.foo.com/?foo=bar', + headers: { + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }, + getSignedRequest: () => { + const signature = + 'b6e3b79003ce0743a491606ba1035a804593b0efb1e20a11cba83f8c25a57a92'; + return { + url: 'https://host.foo.com/?foo=bar', + method: 'POST', + headers: { + Authorization: + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, + host: 'host.foo.com', + date: 'Mon, 09 Sep 2011 23:36:00 GMT', + }, + }; + }, + }, + { + description: 'signed GET request', + referenceDate: new Date('2020-08-11T06:55:22.345Z'), + instance: new AwsRequestSigner(getCredentials, 'us-east-2'), + originalRequest: { + url: + 'https://ec2.us-east-2.amazonaws.com?' + + 'Action=DescribeRegions&Version=2013-10-15', + }, + getSignedRequest: () => { + const amzDate = '20200811T065522Z'; + const dateStamp = '20200811'; + const signature = + '631ea80cddfaa545fdadb120dc92c9f18166e38a5c47b50fab9fce476e022855'; + return { + url: + 'https://ec2.us-east-2.amazonaws.com?' + + 'Action=DescribeRegions&Version=2013-10-15', + method: 'GET', + headers: { + Authorization: + `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + + `${dateStamp}/us-east-2/ec2/aws4_request, SignedHeaders=host;` + + `x-amz-date;x-amz-security-token, Signature=${signature}`, + host: 'ec2.us-east-2.amazonaws.com', + 'x-amz-date': amzDate, + 'x-amz-security-token': token, + }, + }; + }, + }, + { + description: 'signed POST request', + referenceDate: new Date('2020-08-11T06:55:22.345Z'), + instance: new AwsRequestSigner(getCredentials, 'us-east-2'), + originalRequest: { + url: + 'https://sts.us-east-2.amazonaws.com' + + '?Action=GetCallerIdentity&Version=2011-06-15', + method: 'POST', + }, + getSignedRequest: () => { + const amzDate = '20200811T065522Z'; + const dateStamp = '20200811'; + const signature = + '73452984e4a880ffdc5c392355733ec3f5ba310d5e0609a89244440cadfe7a7a'; + return { + url: + 'https://sts.us-east-2.amazonaws.com' + + '?Action=GetCallerIdentity&Version=2011-06-15', + method: 'POST', + headers: { + 'x-amz-date': amzDate, + Authorization: + `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + + `${dateStamp}/us-east-2/sts/aws4_request, SignedHeaders=host;` + + `x-amz-date;x-amz-security-token, Signature=${signature}`, + host: 'sts.us-east-2.amazonaws.com', + 'x-amz-security-token': token, + }, + }; + }, + }, + { + description: 'signed request when AWS credentials have no token', + referenceDate: new Date('2020-08-11T06:55:22.345Z'), + instance: new AwsRequestSigner(getCredentialsWithoutToken, 'us-east-2'), + originalRequest: { + url: + 'https://sts.us-east-2.amazonaws.com' + + '?Action=GetCallerIdentity&Version=2011-06-15', + method: 'POST', + }, + getSignedRequest: () => { + const amzDate = '20200811T065522Z'; + const dateStamp = '20200811'; + const signature = + 'd095ba304919cd0d5570ba8a3787884ee78b860f268ed040ba23831d55536d56'; + return { + url: + 'https://sts.us-east-2.amazonaws.com' + + '?Action=GetCallerIdentity&Version=2011-06-15', + method: 'POST', + headers: { + 'x-amz-date': amzDate, + Authorization: + `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + + `${dateStamp}/us-east-2/sts/aws4_request, SignedHeaders=host;` + + `x-amz-date, Signature=${signature}`, + host: 'sts.us-east-2.amazonaws.com', + }, + }; + }, + }, + { + description: 'signed POST request with additional headers/body', + referenceDate: new Date('2020-08-11T06:55:22.345Z'), + instance: new AwsRequestSigner(getCredentials, 'us-east-2'), + originalRequest: { + url: 'https://dynamodb.us-east-2.amazonaws.com/', + method: 'POST', + headers: { + 'Content-Type': 'application/x-amz-json-1.0', + 'x-amz-target': 'DynamoDB_20120810.CreateTable', + }, + body: JSON.stringify(requestParams), + }, + getSignedRequest: () => { + const amzDate = '20200811T065522Z'; + const dateStamp = '20200811'; + const signature = + 'fdaa5b9cc9c86b80fe61eaf504141c0b3523780349120f2bd8145448456e0385'; + return { + url: 'https://dynamodb.us-east-2.amazonaws.com/', + method: 'POST', + headers: { + Authorization: + `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + + `${dateStamp}/us-east-2/dynamodb/aws4_request, SignedHeaders=` + + 'content-type;host;x-amz-date;x-amz-security-token;x-amz-target' + + `, Signature=${signature}`, + 'Content-Type': 'application/x-amz-json-1.0', + host: 'dynamodb.us-east-2.amazonaws.com', + 'x-amz-date': amzDate, + 'x-amz-security-token': token, + 'x-amz-target': 'DynamoDB_20120810.CreateTable', + }, + body: JSON.stringify(requestParams), + }; + }, + }, + { + description: 'signed POST request with additional headers/data', + referenceDate: new Date('2020-08-11T06:55:22.345Z'), + instance: new AwsRequestSigner(getCredentials, 'us-east-2'), + originalRequest: { + url: 'https://dynamodb.us-east-2.amazonaws.com/', + method: 'POST', + headers: { + 'Content-Type': 'application/x-amz-json-1.0', + 'x-amz-target': 'DynamoDB_20120810.CreateTable', + }, + data: requestParams, + }, + getSignedRequest: () => { + const amzDate = '20200811T065522Z'; + const dateStamp = '20200811'; + const signature = + 'fdaa5b9cc9c86b80fe61eaf504141c0b3523780349120f2bd8145448456e0385'; + return { + url: 'https://dynamodb.us-east-2.amazonaws.com/', + method: 'POST', + headers: { + Authorization: + `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + + `${dateStamp}/us-east-2/dynamodb/aws4_request, SignedHeaders=` + + 'content-type;host;x-amz-date;x-amz-security-token;x-amz-target' + + `, Signature=${signature}`, + 'Content-Type': 'application/x-amz-json-1.0', + host: 'dynamodb.us-east-2.amazonaws.com', + 'x-amz-date': amzDate, + 'x-amz-security-token': token, + 'x-amz-target': 'DynamoDB_20120810.CreateTable', + }, + body: JSON.stringify(requestParams), + }; + }, + }, + ]; + + getRequestOptionsTests.forEach(test => { + it(`should resolve with the expected ${test.description}`, async () => { + clock.tick(test.referenceDate.getTime()); + const actualSignedRequest = await test.instance.getRequestOptions( + test.originalRequest + ); + assert.deepStrictEqual(actualSignedRequest, test.getSignedRequest()); + }); + }); + + it('should reject with underlying getCredentials error', async () => { + const awsRequestSigner = new AwsRequestSigner( + getCredentialsUnsuccessful, + 'us-east-2' + ); + const options: GaxiosOptions = { + url: + 'https://sts.us-east-2.amazonaws.com' + + '?Action=GetCallerIdentity&Version=2011-06-15', + method: 'POST', + }; + + await assert.rejects( + awsRequestSigner.getRequestOptions(options), + awsError + ); + }); + + it('should reject when no URL is available', async () => { + const invalidOptionsError = new Error( + '"url" is required in "amzOptions"' + ); + const awsRequestSigner = new AwsRequestSigner( + getCredentials, + 'us-east-2' + ); + + await assert.rejects( + awsRequestSigner.getRequestOptions({}), + invalidOptionsError + ); + }); + }); +}); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts new file mode 100644 index 00000000..a5ede057 --- /dev/null +++ b/test/test.baseexternalclient.ts @@ -0,0 +1,1847 @@ +// Copyright 2021 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. + +import * as assert from 'assert'; +import {describe, it, afterEach} from 'mocha'; +import * as nock from 'nock'; +import * as sinon from 'sinon'; +import {createCrypto} from '../src/crypto/crypto'; +import {Credentials} from '../src/auth/credentials'; +import {StsSuccessfulResponse} from '../src/auth/stscredentials'; +import { + EXPIRATION_TIME_OFFSET, + BaseExternalAccountClient, +} from '../src/auth/baseexternalclient'; +import { + OAuthErrorResponse, + getErrorFromOAuthErrorResponse, +} from '../src/auth/oauth2common'; +import {GaxiosError} from 'gaxios'; +import { + assertGaxiosResponsePresent, + getAudience, + getTokenUrl, + getServiceAccountImpersonationUrl, + mockCloudResourceManager, + mockGenerateAccessToken, + mockStsTokenExchange, +} from './externalclienthelper'; + +nock.disableNetConnect(); + +interface SampleResponse { + foo: string; + bar: number; +} + +/** Test class to test abstract class ExternalAccountClient. */ +class TestExternalAccountClient extends BaseExternalAccountClient { + private counter = 0; + + async retrieveSubjectToken(): Promise { + // Increment subject_token counter each time this is called. + return `subject_token_${this.counter++}`; + } +} + +const ONE_HOUR_IN_SECS = 3600; + +describe('BaseExternalAccountClient', () => { + let clock: sinon.SinonFakeTimers; + const crypto = createCrypto(); + const audience = getAudience(); + const externalAccountOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: '/var/run/secrets/goog.id/token', + }, + }; + const externalAccountOptionsWithCreds = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: '/var/run/secrets/goog.id/token', + }, + client_id: 'CLIENT_ID', + client_secret: 'SECRET', + }; + const basicAuthCreds = + `${externalAccountOptionsWithCreds.client_id}:` + + `${externalAccountOptionsWithCreds.client_secret}`; + const stsSuccessfulResponse: StsSuccessfulResponse = { + access_token: 'ACCESS_TOKEN', + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: ONE_HOUR_IN_SECS, + scope: 'scope1 scope2', + }; + const externalAccountOptionsWithSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + externalAccountOptions + ); + const externalAccountOptionsWithCredsAndSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + externalAccountOptionsWithCreds + ); + const indeterminableProjectIdAudiences = [ + // Legacy K8s audience format. + 'identitynamespace:1f12345:my_provider', + // Unrealistic audiences. + '//iam.googleapis.com/projects', + '//iam.googleapis.com/projects/', + '//iam.googleapis.com/project/123456', + '//iam.googleapis.com/projects//123456', + '//iam.googleapis.com/prefix_projects/123456', + '//iam.googleapis.com/projects_suffix/123456', + ]; + + afterEach(() => { + nock.cleanAll(); + if (clock) { + clock.restore(); + } + }); + + describe('Constructor', () => { + it('should throw on invalid type', () => { + const expectedError = new Error( + 'Expected "external_account" type but received "invalid"' + ); + const invalidOptions = Object.assign({}, externalAccountOptions); + invalidOptions.type = 'invalid'; + + assert.throws(() => { + return new TestExternalAccountClient(invalidOptions); + }, expectedError); + }); + + it('should not throw on valid options', () => { + assert.doesNotThrow(() => { + return new TestExternalAccountClient(externalAccountOptions); + }); + }); + + it('should set default RefreshOptions', () => { + const client = new TestExternalAccountClient(externalAccountOptions); + + assert(!client.forceRefreshOnFailure); + assert(client.eagerRefreshThresholdMillis === EXPIRATION_TIME_OFFSET); + }); + + it('should set custom RefreshOptions', () => { + const refreshOptions = { + eagerRefreshThresholdMillis: 5000, + forceRefreshOnFailure: true, + }; + const client = new TestExternalAccountClient( + externalAccountOptions, + refreshOptions + ); + + assert.strictEqual( + client.forceRefreshOnFailure, + refreshOptions.forceRefreshOnFailure + ); + assert.strictEqual( + client.eagerRefreshThresholdMillis, + refreshOptions.eagerRefreshThresholdMillis + ); + }); + }); + + describe('projectNumber', () => { + it('should be set if determinable', () => { + const projectNumber = 'my-proj-number'; + const options = Object.assign({}, externalAccountOptions); + options.audience = getAudience(projectNumber); + const client = new TestExternalAccountClient(options); + + assert.equal(client.projectNumber, projectNumber); + }); + + indeterminableProjectIdAudiences.forEach(audience => { + it(`should resolve with null on audience=${audience}`, async () => { + const modifiedOptions = Object.assign({}, externalAccountOptions); + modifiedOptions.audience = audience; + const client = new TestExternalAccountClient(modifiedOptions); + + assert(client.projectNumber === null); + }); + }); + }); + + describe('getProjectId()', () => { + it('should resolve with projectId when determinable', async () => { + const projectNumber = 'my-proj-number'; + const projectId = 'my-proj-id'; + const response = { + projectNumber, + projectId, + lifecycleState: 'ACTIVE', + name: 'project-name', + createTime: '2018-11-06T04:42:54.109Z', + parent: { + type: 'folder', + id: '12345678901', + }, + }; + const options = Object.assign({}, externalAccountOptions); + options.audience = getAudience(projectNumber); + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: options.audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockCloudResourceManager( + projectNumber, + stsSuccessfulResponse.access_token, + 200, + response + ), + ]; + const client = new TestExternalAccountClient(options); + + const actualProjectId = await client.getProjectId(); + + assert.strictEqual(actualProjectId, projectId); + assert.strictEqual(client.projectId, projectId); + + // Next call should return cached result. + const cachedProjectId = await client.getProjectId(); + + assert.strictEqual(cachedProjectId, projectId); + scopes.forEach(scope => scope.done()); + }); + + it('should reject on request error', async () => { + const projectNumber = 'my-proj-number'; + const response = { + error: { + code: 403, + message: 'The caller does not have permission', + status: 'PERMISSION_DENIED', + }, + }; + const options = Object.assign({}, externalAccountOptions); + options.audience = getAudience(projectNumber); + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: options.audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockCloudResourceManager( + projectNumber, + stsSuccessfulResponse.access_token, + 403, + response + ), + ]; + const client = new TestExternalAccountClient(options); + + await assert.rejects( + client.getProjectId(), + /The caller does not have permission/ + ); + + assert.strictEqual(client.projectId, null); + scopes.forEach(scope => scope.done()); + }); + + indeterminableProjectIdAudiences.forEach(audience => { + it(`should resolve with null on audience=${audience}`, async () => { + const modifiedOptions = Object.assign({}, externalAccountOptions); + modifiedOptions.audience = audience; + const client = new TestExternalAccountClient(modifiedOptions); + + const actualProjectId = await client.getProjectId(); + assert(actualProjectId === null); + assert(client.projectId === null); + }); + }); + }); + + describe('getAccessToken()', () => { + describe('without service account impersonation', () => { + it('should resolve with the expected response', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should handle underlying token exchange errors', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + const scope = mockStsTokenExchange([ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + await assert.rejects( + client.getAccessToken(), + getErrorFromOAuthErrorResponse(errorResponse) + ); + // Next try should succeed. + const actualResponse = await client.getAccessToken(); + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should use explicit scopes array when provided', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'scope1 scope2', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + client.scopes = ['scope1', 'scope2']; + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should use explicit scopes string when provided', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'scope1', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + client.scopes = 'scope1'; + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should force refresh when cached credential is expired', async () => { + clock = sinon.useFakeTimers(0); + const emittedEvents: Credentials[] = []; + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; + // Use different expiration time for second token to confirm tokens + // event calculates the credentials expiry_date correctly. + stsSuccessfulResponse2.expires_in = 1600; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + // Listen to tokens events. On every event, push to list of + // emittedEvents. + client.on('tokens', tokens => { + emittedEvents.push(tokens); + }); + const actualResponse = await client.getAccessToken(); + + // tokens event should be triggered once with expected event. + assert.strictEqual(emittedEvents.length, 1); + assert.deepStrictEqual(emittedEvents[0], { + refresh_token: null, + expiry_date: new Date().getTime() + ONE_HOUR_IN_SECS * 1000, + access_token: stsSuccessfulResponse.access_token, + token_type: 'Bearer', + id_token: null, + }); + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + + // Try again. Cached credential should be returned. + clock.tick(ONE_HOUR_IN_SECS * 1000 - EXPIRATION_TIME_OFFSET - 1); + const actualCachedResponse = await client.getAccessToken(); + + // No new event should be triggered since the cached access token is + // returned. + assert.strictEqual(emittedEvents.length, 1); + delete actualCachedResponse.res; + assert.deepStrictEqual(actualCachedResponse, { + token: stsSuccessfulResponse.access_token, + }); + + // Simulate credential is expired. + clock.tick(1); + const actualNewCredResponse = await client.getAccessToken(); + + // tokens event should be triggered again with the expected event. + assert.strictEqual(emittedEvents.length, 2); + assert.deepStrictEqual(emittedEvents[1], { + refresh_token: null, + // Second expiration time should be used. + expiry_date: + new Date().getTime() + stsSuccessfulResponse2.expires_in * 1000, + access_token: stsSuccessfulResponse2.access_token, + token_type: 'Bearer', + id_token: null, + }); + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualNewCredResponse); + delete actualNewCredResponse.res; + assert.deepStrictEqual(actualNewCredResponse, { + token: stsSuccessfulResponse2.access_token, + }); + + scope.done(); + }); + + it('should respect provided eagerRefreshThresholdMillis', async () => { + clock = sinon.useFakeTimers(0); + const customThresh = 10 * 1000; + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions, { + // Override 5min threshold with 10 second threshold. + eagerRefreshThresholdMillis: customThresh, + }); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + + // Try again. Cached credential should be returned. + clock.tick(ONE_HOUR_IN_SECS * 1000 - customThresh - 1); + const actualCachedResponse = await client.getAccessToken(); + + delete actualCachedResponse.res; + assert.deepStrictEqual(actualCachedResponse, { + token: stsSuccessfulResponse.access_token, + }); + + // Simulate credential is expired. + // As current time is equal to expirationTime - customThresh, + // refresh should be triggered. + clock.tick(1); + const actualNewCredResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualNewCredResponse); + delete actualNewCredResponse.res; + assert.deepStrictEqual(actualNewCredResponse, { + token: stsSuccessfulResponse2.access_token, + }); + + scope.done(); + }); + + it('should apply basic auth when credentials are provided', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + additionalHeaders: { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + }, + }, + ]); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithCreds + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + }); + + describe('with service account impersonation', () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const saErrorResponse = { + error: { + code: 400, + message: 'Request contains an invalid argument', + status: 'INVALID_ARGUMENT', + }, + }; + + it('should resolve with the expected response', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSA + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should handle underlying GenerateAccessToken errors', async () => { + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: saErrorResponse.error.code, + response: saErrorResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse2.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSA + ); + await assert.rejects( + client.getAccessToken(), + new RegExp(saErrorResponse.error.message) + ); + // Next try should succeed. + const actualResponse = await client.getAccessToken(); + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should use explicit scopes array when provided', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['scope1', 'scope2'], + }, + ]) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSA + ); + // These scopes should be used for the iamcredentials call. + // https://www.googleapis.com/auth/cloud-platform should be used for the + // STS token exchange request. + client.scopes = ['scope1', 'scope2']; + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should force refresh when cached credential is expired', async () => { + clock = sinon.useFakeTimers(0); + const emittedEvents: Credentials[] = []; + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; + saSuccessResponse.expireTime = new Date( + ONE_HOUR_IN_SECS * 1000 + ).toISOString(); + const saSuccessResponse2 = Object.assign({}, saSuccessResponse); + saSuccessResponse2.accessToken = 'SA_ACCESS_TOKEN2'; + const customExpirationInSecs = 1600; + saSuccessResponse2.expireTime = new Date( + (ONE_HOUR_IN_SECS + customExpirationInSecs) * 1000 + ).toISOString(); + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + { + statusCode: 200, + response: saSuccessResponse2, + token: stsSuccessfulResponse2.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSA + ); + // Listen to tokens events. On every event, push to list of + // emittedEvents. + client.on('tokens', tokens => { + emittedEvents.push(tokens); + }); + const actualResponse = await client.getAccessToken(); + + // tokens event should be triggered once with expected event. + assert.strictEqual(emittedEvents.length, 1); + assert.deepStrictEqual(emittedEvents[0], { + refresh_token: null, + expiry_date: ONE_HOUR_IN_SECS * 1000, + access_token: saSuccessResponse.accessToken, + token_type: 'Bearer', + id_token: null, + }); + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + + // Try again. Cached credential should be returned. + clock.tick(ONE_HOUR_IN_SECS * 1000 - EXPIRATION_TIME_OFFSET - 1); + const actualCachedResponse = await client.getAccessToken(); + + // No new event should be triggered since the cached access token is + // returned. + assert.strictEqual(emittedEvents.length, 1); + delete actualCachedResponse.res; + assert.deepStrictEqual(actualCachedResponse, { + token: saSuccessResponse.accessToken, + }); + + // Simulate credential is expired. + clock.tick(1); + const actualNewCredResponse = await client.getAccessToken(); + + // tokens event should be triggered again with the expected event. + assert.strictEqual(emittedEvents.length, 2); + assert.deepStrictEqual(emittedEvents[1], { + refresh_token: null, + // Second expiration time should be used. + expiry_date: (ONE_HOUR_IN_SECS + customExpirationInSecs) * 1000, + access_token: saSuccessResponse2.accessToken, + token_type: 'Bearer', + id_token: null, + }); + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualNewCredResponse); + delete actualNewCredResponse.res; + assert.deepStrictEqual(actualNewCredResponse, { + token: saSuccessResponse2.accessToken, + }); + + scopes.forEach(scope => scope.done()); + }); + + it('should respect provided eagerRefreshThresholdMillis', async () => { + clock = sinon.useFakeTimers(0); + const customThresh = 10 * 1000; + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; + saSuccessResponse.expireTime = new Date( + ONE_HOUR_IN_SECS * 1000 + ).toISOString(); + const saSuccessResponse2 = Object.assign({}, saSuccessResponse); + saSuccessResponse2.accessToken = 'SA_ACCESS_TOKEN2'; + saSuccessResponse2.expireTime = new Date( + 2 * ONE_HOUR_IN_SECS * 1000 + ).toISOString(); + + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + { + statusCode: 200, + response: saSuccessResponse2, + token: stsSuccessfulResponse2.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSA, + { + // Override 5min threshold with 10 second threshold. + eagerRefreshThresholdMillis: customThresh, + } + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + + // Try again. Cached credential should be returned. + clock.tick(ONE_HOUR_IN_SECS * 1000 - customThresh - 1); + const actualCachedResponse = await client.getAccessToken(); + + delete actualCachedResponse.res; + assert.deepStrictEqual(actualCachedResponse, { + token: saSuccessResponse.accessToken, + }); + + // Simulate credential is expired. + // As current time is equal to expirationTime - customThresh, + // refresh should be triggered. + clock.tick(1); + const actualNewCredResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualNewCredResponse); + delete actualNewCredResponse.res; + assert.deepStrictEqual(actualNewCredResponse, { + token: saSuccessResponse2.accessToken, + }); + + scopes.forEach(scope => scope.done()); + }); + + it('should apply basic auth when credentials are provided', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + additionalHeaders: { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithCredsAndSA + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + }); + }); + + describe('getRequestHeaders()', () => { + it('should inject the authorization headers', async () => { + const expectedHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + const actualHeaders = await client.getRequestHeaders(); + + assert.deepStrictEqual(actualHeaders, expectedHeaders); + scope.done(); + }); + + it('should inject service account access token in headers', async () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const expectedHeaders = { + Authorization: `Bearer ${saSuccessResponse.accessToken}`, + }; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSA + ); + const actualHeaders = await client.getRequestHeaders(); + + assert.deepStrictEqual(actualHeaders, expectedHeaders); + scopes.forEach(scope => scope.done()); + }); + + it('should inject the authorization and metadata headers', async () => { + const quotaProjectId = 'QUOTA_PROJECT_ID'; + const expectedHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + 'x-goog-user-project': quotaProjectId, + }; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const optionsWithQuotaProjectId = Object.assign( + {quota_project_id: quotaProjectId}, + externalAccountOptions + ); + const client = new TestExternalAccountClient(optionsWithQuotaProjectId); + const actualHeaders = await client.getRequestHeaders(); + + assert.deepStrictEqual(expectedHeaders, actualHeaders); + scope.done(); + }); + + it('should reject when error occurs during token retrieval', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + const scope = mockStsTokenExchange([ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + await assert.rejects( + client.getRequestHeaders(), + getErrorFromOAuthErrorResponse(errorResponse) + ); + scope.done(); + }); + }); + + describe('request()', () => { + it('should process HTTP request with authorization header', async () => { + const quotaProjectId = 'QUOTA_PROJECT_ID'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + 'x-goog-user-project': quotaProjectId, + }; + const optionsWithQuotaProjectId = Object.assign( + {quota_project_id: quotaProjectId}, + externalAccountOptions + ); + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new TestExternalAccountClient(optionsWithQuotaProjectId); + const actualResponse = await client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should inject service account access token in headers', async () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const quotaProjectId = 'QUOTA_PROJECT_ID'; + const authHeaders = { + Authorization: `Bearer ${saSuccessResponse.accessToken}`, + 'x-goog-user-project': quotaProjectId, + }; + const optionsWithQuotaProjectId = Object.assign( + {quota_project_id: quotaProjectId}, + externalAccountOptionsWithSA + ); + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new TestExternalAccountClient(optionsWithQuotaProjectId); + const actualResponse = await client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should process headerless HTTP request', async () => { + const quotaProjectId = 'QUOTA_PROJECT_ID'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + 'x-goog-user-project': quotaProjectId, + }; + const optionsWithQuotaProjectId = Object.assign( + {quota_project_id: quotaProjectId}, + externalAccountOptions + ); + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new TestExternalAccountClient(optionsWithQuotaProjectId); + // Send request with no headers. + const actualResponse = await client.request({ + url: 'https://example.com/api', + method: 'POST', + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should reject when error occurs during token retrieval', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const scope = mockStsTokenExchange([ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + await assert.rejects( + client.request({ + url: 'https://example.com/api', + method: 'POST', + data: exampleRequest, + responseType: 'json', + }), + getErrorFromOAuthErrorResponse(errorResponse) + ); + scope.done(); + }); + + it('should trigger callback on success when provided', done => { + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new TestExternalAccountClient(externalAccountOptions); + client.request( + { + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }, + (err, result) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(result?.data, exampleResponse); + scopes.forEach(scope => scope.done()); + done(); + } + ); + }); + + it('should trigger callback on error when provided', done => { + const errorMessage = 'Bad Request'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(400, errorMessage), + ]; + + const client = new TestExternalAccountClient(externalAccountOptions); + client.request( + { + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }, + (err, result) => { + assert.strictEqual(err!.message, errorMessage); + assert.deepStrictEqual(result, (err as GaxiosError)!.response); + scopes.forEach(scope => scope.done()); + done(); + } + ); + }); + + it('should retry on 401 on forceRefreshOnFailure=true', async () => { + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const authHeaders2 = { + Authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(401) + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders2), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new TestExternalAccountClient(externalAccountOptions, { + forceRefreshOnFailure: true, + }); + const actualResponse = await client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should not retry on 401 on forceRefreshOnFailure=false', async () => { + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(401), + ]; + + const client = new TestExternalAccountClient(externalAccountOptions); + await assert.rejects( + client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }), + { + code: '401', + } + ); + + scopes.forEach(scope => scope.done()); + }); + + it('should not retry more than once', async () => { + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const authHeaders2 = { + Authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(403) + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders2), + }) + .reply(403), + ]; + + const client = new TestExternalAccountClient(externalAccountOptions, { + forceRefreshOnFailure: true, + }); + await assert.rejects( + client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }), + { + code: '403', + } + ); + + scopes.forEach(scope => scope.done()); + }); + }); + + describe('setCredentials()', () => { + it('should allow injection of GCP access tokens directly', async () => { + clock = sinon.useFakeTimers(0); + const credentials = { + access_token: 'INJECTED_ACCESS_TOKEN', + // Simulate token expires in 10mins. + expiry_date: new Date().getTime() + 10 * 60 * 1000, + }; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient(externalAccountOptions); + client.setCredentials(credentials); + + clock.tick(10 * 60 * 1000 - EXPIRATION_TIME_OFFSET - 1); + const tokenResponse = await client.getAccessToken(); + assert.deepStrictEqual(tokenResponse.token, credentials.access_token); + + // Simulate token expired. + clock.tick(1); + const refreshedTokenResponse = await client.getAccessToken(); + assert.deepStrictEqual( + refreshedTokenResponse.token, + stsSuccessfulResponse.access_token + ); + + scope.done(); + }); + + it('should not expire injected creds with no expiry_date', async () => { + clock = sinon.useFakeTimers(0); + const credentials = { + access_token: 'INJECTED_ACCESS_TOKEN', + }; + + const client = new TestExternalAccountClient(externalAccountOptions); + client.setCredentials(credentials); + + const tokenResponse = await client.getAccessToken(); + assert.deepStrictEqual(tokenResponse.token, credentials.access_token); + + clock.tick(ONE_HOUR_IN_SECS); + const unexpiredTokenResponse = await client.getAccessToken(); + assert.deepStrictEqual( + unexpiredTokenResponse.token, + credentials.access_token + ); + }); + }); +}); diff --git a/test/test.crypto.ts b/test/test.crypto.ts index f4faf670..101d94c8 100644 --- a/test/test.crypto.ts +++ b/test/test.crypto.ts @@ -1,12 +1,41 @@ +// Copyright 2020 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. + import * as fs from 'fs'; import {assert} from 'chai'; import {describe, it} from 'mocha'; -import {createCrypto} from '../src/crypto/crypto'; +import {createCrypto, fromArrayBufferToHex} from '../src/crypto/crypto'; import {NodeCrypto} from '../src/crypto/node/crypto'; const publicKey = fs.readFileSync('./test/fixtures/public.pem', 'utf-8'); const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); +/** + * Converts a Node.js Buffer to an ArrayBuffer. + * https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer + * @param buffer The Buffer input to covert. + * @return The ArrayBuffer representation of the input. + */ +function toArrayBuffer(buffer: Buffer): ArrayBuffer { + const arrayBuffer = new ArrayBuffer(buffer.length); + const arrayBufferView = new Uint8Array(arrayBuffer); + for (let i = 0; i < buffer.length; i++) { + arrayBufferView[i] = buffer[i]; + } + return arrayBuffer; +} + describe('crypto', () => { const crypto = createCrypto(); @@ -80,4 +109,54 @@ describe('crypto', () => { const hits = loadedModules.filter(x => x.includes('fast-text-encoding')); assert.strictEqual(hits.length, 0); }); + + it('should calculate SHA256 digest in hex encoding', async () => { + const input = 'I can calculate SHA256'; + const expectedHexDigest = + '73d08486d8bfd4fb4bc12dd8903604ddbde5ad95b6efa567bd723ce81a881122'; + + const calculatedHexDigest = await crypto.sha256DigestHex(input); + assert.strictEqual(calculatedHexDigest, expectedHexDigest); + }); + + describe('should compute the HMAC-SHA256 hash of a message', () => { + it('using string key', async () => { + const message = 'The quick brown fox jumps over the lazy dog'; + const key = 'key'; + const expectedHexHash = + 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'; + const extectedHash = new Uint8Array( + (expectedHexHash.match(/.{1,2}/g) as string[]).map(byte => + parseInt(byte, 16) + ) + ); + + const calculatedHash = await crypto.signWithHmacSha256(key, message); + assert.deepStrictEqual(calculatedHash, extectedHash.buffer); + }); + + it('using an ArrayBuffer key', async () => { + const message = 'The quick brown fox jumps over the lazy dog'; + const key = toArrayBuffer(Buffer.from('key')); + const expectedHexHash = + 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'; + const extectedHash = new Uint8Array( + (expectedHexHash.match(/.{1,2}/g) as string[]).map(byte => + parseInt(byte, 16) + ) + ); + + const calculatedHash = await crypto.signWithHmacSha256(key, message); + assert.deepStrictEqual(calculatedHash, extectedHash.buffer); + }); + }); + + it('should expose a method to convert an ArrayBuffer to hex', () => { + const arrayBuffer = new Uint8Array([4, 8, 0, 12, 16, 0]) + .buffer as ArrayBuffer; + const expectedHexEncoding = '0408000c1000'; + + const calculatedHexEncoding = fromArrayBufferToHex(arrayBuffer); + assert.strictEqual(calculatedHexEncoding, expectedHexEncoding); + }); }); diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts new file mode 100644 index 00000000..4d1f95fe --- /dev/null +++ b/test/test.externalclient.ts @@ -0,0 +1,154 @@ +// Copyright 2021 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. + +import * as assert from 'assert'; +import {describe, it} from 'mocha'; +import {AwsClient} from '../src/auth/awsclient'; +import {IdentityPoolClient} from '../src/auth/identitypoolclient'; +import {ExternalAccountClient} from '../src/auth/externalclient'; +import {getAudience, getTokenUrl} from './externalclienthelper'; + +const serviceAccountKeys = { + type: 'service_account', + project_id: 'PROJECT_ID', + private_key_id: 'PRIVATE_KEY_ID', + private_key: + '-----BEGIN PRIVATE KEY-----\n' + 'REDACTED\n-----END PRIVATE KEY-----\n', + client_email: '$PROJECT_ID@appspot.gserviceaccount.com', + client_id: 'CLIENT_ID', + auth_uri: 'https://accounts.google.com/o/oauth2/auth', + token_uri: 'https://accounts.google.com/o/oauth2/token', + auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs', + client_x509_cert_url: + 'https://www.googleapis.com/robot/v1/metadata/x509/' + + 'PROEJCT_ID%40appspot.gserviceaccount.com', +}; + +const fileSourcedOptions = { + type: 'external_account', + audience: getAudience(), + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: './test/fixtures/external-subject-token.txt', + }, +}; + +const metadataBaseUrl = 'http://169.254.169.254'; +const awsCredentialSource = { + environment_id: 'aws1', + region_url: `${metadataBaseUrl}/latest/meta-data/placement/availability-zone`, + url: `${metadataBaseUrl}/latest/meta-data/iam/security-credentials`, + regional_cred_verification_url: + 'https://sts.{region}.amazonaws.com?' + + 'Action=GetCallerIdentity&Version=2011-06-15', +}; +const awsOptions = { + type: 'external_account', + audience: getAudience(), + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: awsCredentialSource, +}; + +describe('ExternalAccountClient', () => { + describe('Constructor', () => { + it('should throw on initialization', () => { + assert.throws(() => { + return new ExternalAccountClient(); + }, /ExternalAccountClients should be initialized via/); + }); + }); + + describe('fromJSON()', () => { + const refreshOptions = { + eagerRefreshThresholdMillis: 1000 * 10, + forceRefreshOnFailure: true, + }; + + it('should return IdentityPoolClient on IdentityPoolClientOptions', () => { + const expectedClient = new IdentityPoolClient(fileSourcedOptions); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(fileSourcedOptions), + expectedClient + ); + }); + + it('should return IdentityPoolClient with expected RefreshOptions', () => { + const expectedClient = new IdentityPoolClient( + fileSourcedOptions, + refreshOptions + ); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(fileSourcedOptions, refreshOptions), + expectedClient + ); + }); + + it('should return AwsClient on AwsClientOptions', () => { + const expectedClient = new AwsClient(awsOptions); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(awsOptions), + expectedClient + ); + }); + + it('should return AwsClient with expected RefreshOptions', () => { + const expectedClient = new AwsClient(awsOptions, refreshOptions); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(awsOptions, refreshOptions), + expectedClient + ); + }); + + it('should return null when given non-ExternalAccountClientOptions', () => { + assert( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ExternalAccountClient.fromJSON(serviceAccountKeys as any) === null + ); + }); + + it('should throw when given invalid ExternalAccountClient', () => { + const invalidOptions = Object.assign({}, fileSourcedOptions); + delete invalidOptions.credential_source; + + assert.throws(() => { + return ExternalAccountClient.fromJSON(invalidOptions); + }); + }); + + it('should throw when given invalid IdentityPoolClient', () => { + const invalidOptions = Object.assign({}, fileSourcedOptions); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (invalidOptions as any).credential_source = {}; + + assert.throws(() => { + return ExternalAccountClient.fromJSON(invalidOptions); + }); + }); + + it('should throw when given invalid AwsClientOptions', () => { + const invalidOptions = Object.assign({}, awsOptions); + invalidOptions.credential_source.environment_id = 'invalid'; + + assert.throws(() => { + return ExternalAccountClient.fromJSON(invalidOptions); + }); + }); + }); +}); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index e1039ba3..a3c78eb6 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -30,10 +30,25 @@ import * as os from 'os'; import * as path from 'path'; import * as sinon from 'sinon'; -import {GoogleAuth, JWT, UserRefreshClient, IdTokenClient} from '../src'; +import { + GoogleAuth, + JWT, + UserRefreshClient, + IdTokenClient, + ExternalAccountClient, + OAuth2Client, + ExternalAccountClientOptions, + RefreshOptions, +} from '../src'; import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; import {Compute} from '../src/auth/computeclient'; +import { + mockCloudResourceManager, + mockStsTokenExchange, +} from './externalclienthelper'; +import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; +import {AuthClient} from '../src/auth/authclient'; nock.disableNetConnect(); @@ -61,6 +76,8 @@ describe('googleauth', () => { const private2JSON = require('../../test/fixtures/private2.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires const refreshJSON = require('../../test/fixtures/refresh.json'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const externalAccountJSON = require('../../test/fixtures/external-account-cred.json'); const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); const wellKnownPathWindows = path.join( 'C:', @@ -167,10 +184,11 @@ describe('googleauth', () => { fs.createReadStream('./test/fixtures/private2.json'); } - function mockLinuxWellKnownFile() { + function mockLinuxWellKnownFile( + filePath = './test/fixtures/private2.json' + ) { exposeLinuxWellKnownFile = true; - createLinuxWellKnownStream = () => - fs.createReadStream('./test/fixtures/private2.json'); + createLinuxWellKnownStream = () => fs.createReadStream(filePath); } function nockIsGCE() { @@ -969,7 +987,7 @@ describe('googleauth', () => { // a JWTClient. assert.strictEqual( 'compute-placeholder', - res.credential.credentials.refresh_token + (res.credential as OAuth2Client).credentials.refresh_token ); }); @@ -1531,6 +1549,741 @@ describe('googleauth', () => { .post('/token') .reply(200, {}); } + + describe('for external_account types', () => { + let fromJsonSpy: sinon.SinonSpy< + [ExternalAccountClientOptions, RefreshOptions?], + BaseExternalAccountClient | null + >; + const stsSuccessfulResponse = { + access_token: 'ACCESS_TOKEN', + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: 3600, + scope: 'scope1 scope2', + }; + const fileSubjectToken = fs.readFileSync( + externalAccountJSON.credential_source.file, + 'utf-8' + ); + // Project number should match the project number in externalAccountJSON. + const projectNumber = '123456'; + const projectId = 'my-proj-id'; + const projectInfoResponse = { + projectNumber, + projectId, + lifecycleState: 'ACTIVE', + name: 'project-name', + createTime: '2018-11-06T04:42:54.109Z', + parent: { + type: 'folder', + id: '12345678901', + }, + }; + const refreshOptions = { + eagerRefreshThresholdMillis: 5000, + forceRefreshOnFailure: true, + }; + const defaultScopes = ['http://examples.com/is/a/default/scope']; + const userScopes = ['http://examples.com/is/a/scope']; + + /** + * @return A copy of the external account JSON auth object for testing. + */ + function createExternalAccountJSON() { + const credentialSourceCopy = Object.assign( + {}, + externalAccountJSON.credential_source + ); + const jsonCopy = Object.assign({}, externalAccountJSON); + jsonCopy.credential_source = credentialSourceCopy; + return jsonCopy; + } + + /** + * Creates mock HTTP handlers for retrieving access tokens and + * optional ones for retrieving the project ID via cloud resource + * manager. + * @param mockProjectIdRetrieval Whether to mock project ID retrieval. + * @param expectedScopes The list of expected scopes. + * @return The list of nock.Scope corresponding to the mocked HTTP + * requests. + */ + function mockGetAccessTokenAndProjectId( + mockProjectIdRetrieval = true, + expectedScopes = ['https://www.googleapis.com/auth/cloud-platform'] + ): nock.Scope[] { + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: externalAccountJSON.audience, + scope: expectedScopes.join(' '), + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: fileSubjectToken, + subject_token_type: externalAccountJSON.subject_token_type, + }, + }, + ]), + ]; + + if (mockProjectIdRetrieval) { + scopes.push( + mockCloudResourceManager( + projectNumber, + stsSuccessfulResponse.access_token, + 200, + projectInfoResponse + ) + ); + } + + return scopes; + } + + /** + * Asserts that the provided client was initialized with the expected + * JSON object and RefreshOptions. + * @param actualClient The actual client to assert. + * @param json The expected JSON object that the client should be + * initialized with. + * @param options The expected RefreshOptions the client should be + * initialized with. + */ + function assertExternalAccountClientInitialized( + actualClient: AuthClient, + json: ExternalAccountClientOptions, + options: RefreshOptions + ) { + // Confirm expected client is initialized. + assert(fromJsonSpy.calledOnceWithExactly(json, options)); + assert(fromJsonSpy.returned(actualClient as BaseExternalAccountClient)); + } + + beforeEach(() => { + // Listen to external account initializations. + // This is useful to confirm that a GoogleAuth returned client is + // an external account initialized with the expected parameters. + fromJsonSpy = sinon.spy(ExternalAccountClient, 'fromJSON'); + }); + + afterEach(() => { + fromJsonSpy.restore(); + }); + + describe('fromJSON()', () => { + it('should create the expected BaseExternalAccountClient', () => { + const json = createExternalAccountJSON(); + const result = auth.fromJSON(json); + + assertExternalAccountClientInitialized(result, json, {}); + }); + + it('should honor defaultScopes when no user scopes are available', () => { + const json = createExternalAccountJSON(); + auth.defaultScopes = defaultScopes; + const result = auth.fromJSON(json); + + assertExternalAccountClientInitialized(result, json, {}); + assert.strictEqual( + (result as BaseExternalAccountClient).scopes, + defaultScopes + ); + }); + + it('should prefer user scopes over defaultScopes', () => { + const json = createExternalAccountJSON(); + const auth = new GoogleAuth({scopes: userScopes}); + auth.defaultScopes = defaultScopes; + const result = auth.fromJSON(json); + + assertExternalAccountClientInitialized(result, json, {}); + assert.strictEqual( + (result as BaseExternalAccountClient).scopes, + userScopes + ); + }); + + it('should create client with custom RefreshOptions', () => { + const json = createExternalAccountJSON(); + const result = auth.fromJSON(json, refreshOptions); + + assertExternalAccountClientInitialized(result, json, refreshOptions); + }); + + it('should throw on invalid json', () => { + const invalidJson = createExternalAccountJSON(); + delete invalidJson.credential_source; + const auth = new GoogleAuth(); + + assert.throws(() => { + auth.fromJSON(invalidJson); + }); + }); + }); + + describe('fromStream()', () => { + it('should read the stream and create a client', async () => { + const stream = fs.createReadStream( + './test/fixtures/external-account-cred.json' + ); + const actualClient = await auth.fromStream(stream); + + assertExternalAccountClientInitialized( + actualClient, + createExternalAccountJSON(), + {} + ); + }); + + it('should include provided RefreshOptions in client', async () => { + const stream = fs.createReadStream( + './test/fixtures/external-account-cred.json' + ); + const auth = new GoogleAuth(); + const result = await auth.fromStream(stream, refreshOptions); + + assertExternalAccountClientInitialized( + result, + createExternalAccountJSON(), + refreshOptions + ); + }); + }); + + describe('getApplicationDefault()', () => { + it('should use environment variable when it is set', async () => { + const scopes = mockGetAccessTokenAndProjectId(); + // Environment variable is set up to point to + // external-account-cred.json + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-cred.json' + ); + + const res = await auth.getApplicationDefault(); + const client = res.credential; + + assertExternalAccountClientInitialized( + client, + createExternalAccountJSON(), + {} + ); + // Project ID should also be set. + assert.deepEqual(client.projectId, projectId); + scopes.forEach(s => s.done()); + }); + + it('should use defaultScopes for environment variable ADC', async () => { + const scopes = mockGetAccessTokenAndProjectId(true, defaultScopes); + // Environment variable is set up to point to + // external-account-cred.json + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-cred.json' + ); + + const auth = new GoogleAuth(); + auth.defaultScopes = defaultScopes; + const res = await auth.getApplicationDefault(); + const client = res.credential; + + assertExternalAccountClientInitialized( + client, + createExternalAccountJSON(), + {} + ); + assert.strictEqual( + (client as BaseExternalAccountClient).scopes, + defaultScopes + ); + scopes.forEach(s => s.done()); + }); + + it('should prefer user scopes over defaultScopes for environment variable ADC', async () => { + const scopes = mockGetAccessTokenAndProjectId(true, userScopes); + // Environment variable is set up to point to + // external-account-cred.json + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-cred.json' + ); + + const auth = new GoogleAuth({scopes: userScopes}); + auth.defaultScopes = defaultScopes; + const res = await auth.getApplicationDefault(); + const client = res.credential; + + assertExternalAccountClientInitialized( + client, + createExternalAccountJSON(), + {} + ); + assert.strictEqual( + (client as BaseExternalAccountClient).scopes, + userScopes + ); + scopes.forEach(s => s.done()); + }); + + it('should use well-known file when it is available and env const is not set', async () => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is set up to point to external-account-cred.json + mockLinuxWellKnownFile('./test/fixtures/external-account-cred.json'); + const scopes = mockGetAccessTokenAndProjectId(); + + const res = await auth.getApplicationDefault(); + const client = res.credential; + + assertExternalAccountClientInitialized( + client, + createExternalAccountJSON(), + {} + ); + assert.deepEqual(client.projectId, projectId); + scopes.forEach(s => s.done()); + }); + + it('should use defaultScopes for well-known file ADC', async () => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is set up to point to external-account-cred.json + mockLinuxWellKnownFile('./test/fixtures/external-account-cred.json'); + const scopes = mockGetAccessTokenAndProjectId(true, defaultScopes); + + const auth = new GoogleAuth(); + auth.defaultScopes = defaultScopes; + const res = await auth.getApplicationDefault(); + const client = res.credential; + + assertExternalAccountClientInitialized( + client, + createExternalAccountJSON(), + {} + ); + assert.strictEqual( + (client as BaseExternalAccountClient).scopes, + defaultScopes + ); + scopes.forEach(s => s.done()); + }); + + it('should prefer user scopes over defaultScopes for well-known file ADC', async () => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is set up to point to external-account-cred.json + mockLinuxWellKnownFile('./test/fixtures/external-account-cred.json'); + const scopes = mockGetAccessTokenAndProjectId(true, userScopes); + + const auth = new GoogleAuth({scopes: userScopes}); + auth.defaultScopes = defaultScopes; + const res = await auth.getApplicationDefault(); + const client = res.credential; + + assertExternalAccountClientInitialized( + client, + createExternalAccountJSON(), + {} + ); + assert.strictEqual( + (client as BaseExternalAccountClient).scopes, + userScopes + ); + scopes.forEach(s => s.done()); + }); + }); + + describe('getApplicationCredentialsFromFilePath()', () => { + it('should correctly read the file and create a valid client', async () => { + const actualClient = await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/external-account-cred.json' + ); + + assertExternalAccountClientInitialized( + actualClient, + createExternalAccountJSON(), + {} + ); + }); + + it('should include provided RefreshOptions in client', async () => { + const result = await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/external-account-cred.json', + refreshOptions + ); + + assertExternalAccountClientInitialized( + result, + createExternalAccountJSON(), + refreshOptions + ); + }); + }); + + describe('getProjectId()', () => { + it('should get projectId from cloud resource manager', async () => { + const scopes = mockGetAccessTokenAndProjectId(); + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({keyFilename}); + const actualProjectId = await auth.getProjectId(); + + assert.deepEqual(actualProjectId, projectId); + scopes.forEach(s => s.done()); + }); + + it('should prioritize explicitly provided projectId', async () => { + const explicitProjectId = 'my-explictly-specified-project-id'; + const auth = new GoogleAuth({ + credentials: createExternalAccountJSON(), + projectId: explicitProjectId, + }); + const actualProjectId = await auth.getProjectId(); + + assert.deepEqual(actualProjectId, explicitProjectId); + }); + + it('should reject when client.getProjectId() fails', async () => { + const scopes = mockGetAccessTokenAndProjectId(false); + scopes.push( + mockCloudResourceManager( + projectNumber, + stsSuccessfulResponse.access_token, + 403, + { + error: { + code: 403, + message: 'The caller does not have permission', + status: 'PERMISSION_DENIED', + }, + } + ) + ); + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({keyFilename}); + + await assert.rejects( + auth.getProjectId(), + /Unable to detect a Project Id in the current environment/ + ); + scopes.forEach(s => s.done()); + }); + + it('should reject on invalid external_account client', async () => { + const invalidOptions = createExternalAccountJSON(); + invalidOptions.credential_source.file = 'invalid'; + const auth = new GoogleAuth({credentials: invalidOptions}); + + await assert.rejects( + auth.getProjectId(), + /Unable to detect a Project Id in the current environment/ + ); + }); + + it('should reject when projectId not determinable', async () => { + const json = createExternalAccountJSON(); + json.audience = 'identitynamespace:1f12345:my_provider'; + const auth = new GoogleAuth({credentials: json}); + + await assert.rejects( + auth.getProjectId(), + /Unable to detect a Project Id in the current environment/ + ); + }); + }); + + it('tryGetApplicationCredentialsFromEnvironmentVariable() should resolve', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-cred.json' + ); + const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable( + refreshOptions + ); + + assert(result); + assertExternalAccountClientInitialized( + result as AuthClient, + createExternalAccountJSON(), + refreshOptions + ); + }); + + it('tryGetApplicationCredentialsFromWellKnownFile() should resolve', async () => { + // Set up a mock to return path to a valid credentials file. + mockLinuxWellKnownFile('./test/fixtures/external-account-cred.json'); + const result = await auth._tryGetApplicationCredentialsFromWellKnownFile( + refreshOptions + ); + + assert(result); + assertExternalAccountClientInitialized( + result as AuthClient, + createExternalAccountJSON(), + refreshOptions + ); + }); + + it('getApplicationCredentialsFromFilePath() should resolve', async () => { + const result = await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/external-account-cred.json', + refreshOptions + ); + + assertExternalAccountClientInitialized( + result, + createExternalAccountJSON(), + refreshOptions + ); + }); + + describe('getClient()', () => { + it('should initialize from credentials', async () => { + const auth = new GoogleAuth({ + credentials: createExternalAccountJSON(), + }); + const actualClient = await auth.getClient(); + + assertExternalAccountClientInitialized( + actualClient, + createExternalAccountJSON(), + {} + ); + }); + + it('should initialize from keyFileName', async () => { + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({keyFilename}); + const actualClient = await auth.getClient(); + + assertExternalAccountClientInitialized( + actualClient, + createExternalAccountJSON(), + {} + ); + }); + + it('should initialize from ADC', async () => { + const scopes = mockGetAccessTokenAndProjectId(); + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-cred.json' + ); + const auth = new GoogleAuth(); + const client = await auth.getClient(); + + assertExternalAccountClientInitialized( + client, + createExternalAccountJSON(), + {} + ); + scopes.forEach(s => s.done()); + }); + + it('should allow use defaultScopes when no scopes are available', async () => { + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({keyFilename}); + // Set defaultScopes on Auth instance. This should be set on the + // underlying client. + auth.defaultScopes = defaultScopes; + const client = (await auth.getClient()) as BaseExternalAccountClient; + + assert.strictEqual(client.scopes, defaultScopes); + }); + + it('should prefer user scopes over defaultScopes', async () => { + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({scopes: userScopes, keyFilename}); + // Set defaultScopes on Auth instance. User scopes should be used. + auth.defaultScopes = defaultScopes; + const client = (await auth.getClient()) as BaseExternalAccountClient; + + assert.strictEqual(client.scopes, userScopes); + }); + + it('should allow passing scopes to get a client', async () => { + const scopes = ['http://examples.com/is/a/scope']; + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({scopes, keyFilename}); + const client = (await auth.getClient()) as BaseExternalAccountClient; + + assert.strictEqual(client.scopes, scopes); + }); + + it('should allow passing a scope to get a client', async () => { + const scopes = 'http://examples.com/is/a/scope'; + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({scopes, keyFilename}); + const client = (await auth.getClient()) as BaseExternalAccountClient; + + assert.strictEqual(client.scopes, scopes); + }); + }); + + it('getIdTokenClient() should reject', async () => { + const auth = new GoogleAuth({credentials: createExternalAccountJSON()}); + + await assert.rejects( + auth.getIdTokenClient('a-target-audience'), + /Cannot fetch ID token in this environment/ + ); + }); + + it('sign() should reject', async () => { + const scopes = mockGetAccessTokenAndProjectId(); + const auth = new GoogleAuth({credentials: createExternalAccountJSON()}); + + await assert.rejects( + auth.sign('abc123'), + /Cannot sign data without `client_email`/ + ); + scopes.forEach(s => s.done()); + }); + + it('getAccessToken() should get an access token', async () => { + const scopes = mockGetAccessTokenAndProjectId(false); + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({keyFilename}); + const token = await auth.getAccessToken(); + + assert.strictEqual(token, stsSuccessfulResponse.access_token); + scopes.forEach(s => s.done()); + }); + + it('getRequestHeaders() should inject authorization header', async () => { + const scopes = mockGetAccessTokenAndProjectId(false); + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({keyFilename}); + const headers = await auth.getRequestHeaders(); + + assert.deepStrictEqual(headers, { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }); + scopes.forEach(s => s.done()); + }); + + it('authorizeRequest() should authorize the request', async () => { + const scopes = mockGetAccessTokenAndProjectId(false); + const keyFilename = './test/fixtures/external-account-cred.json'; + const auth = new GoogleAuth({keyFilename}); + const opts = await auth.authorizeRequest({url: 'http://example.com'}); + + assert.deepStrictEqual(opts.headers, { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }); + scopes.forEach(s => s.done()); + }); + + it('request() should make the request with auth header', async () => { + const url = 'http://example.com'; + const data = {breakfast: 'coffee'}; + const keyFilename = './test/fixtures/external-account-cred.json'; + const scopes = mockGetAccessTokenAndProjectId(false); + scopes.push( + nock(url) + .get('/', undefined, { + reqheaders: { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }, + }) + .reply(200, data) + ); + + const auth = new GoogleAuth({keyFilename}); + const res = await auth.request({url}); + + assert.deepStrictEqual(res.data, data); + scopes.forEach(s => s.done()); + }); + }); + }); + + // Allows a client to be instantiated from a certificate, + // See: https://github.com/googleapis/google-auth-library-nodejs/issues/808 + it('allows client to be instantiated from PEM key file', async () => { + const auth = new GoogleAuth({ + keyFile: PEM_PATH, + clientOptions: { + scopes: 'http://foo', + email: 'foo@serviceaccount.com', + subject: 'bar@subjectaccount.com', + }, + }); + const jwt = await auth.getClient(); + const scope = createGTokenMock({access_token: 'initial-access-token'}); + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual( + headers.Authorization, + 'Bearer initial-access-token' + ); + scope.done(); + assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); + }); + it('allows client to be instantiated from p12 key file', async () => { + const auth = new GoogleAuth({ + keyFile: P12_PATH, + clientOptions: { + scopes: 'http://foo', + email: 'foo@serviceaccount.com', + subject: 'bar@subjectaccount.com', + }, + }); + const jwt = await auth.getClient(); + const scope = createGTokenMock({access_token: 'initial-access-token'}); + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual( + headers.Authorization, + 'Bearer initial-access-token' + ); + scope.done(); + assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); + }); + + // Allows a client to be instantiated from a certificate, + // See: https://github.com/googleapis/google-auth-library-nodejs/issues/808 + it('allows client to be instantiated from PEM key file', async () => { + const auth = new GoogleAuth({ + keyFile: PEM_PATH, + clientOptions: { + scopes: 'http://foo', + email: 'foo@serviceaccount.com', + subject: 'bar@subjectaccount.com', + }, + }); + const jwt = await auth.getClient(); + const scope = createGTokenMock({access_token: 'initial-access-token'}); + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual( + headers.Authorization, + 'Bearer initial-access-token' + ); + scope.done(); + assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); + }); + it('allows client to be instantiated from p12 key file', async () => { + const auth = new GoogleAuth({ + keyFile: P12_PATH, + clientOptions: { + scopes: 'http://foo', + email: 'foo@serviceaccount.com', + subject: 'bar@subjectaccount.com', + }, + }); + const jwt = await auth.getClient(); + const scope = createGTokenMock({access_token: 'initial-access-token'}); + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual( + headers.Authorization, + 'Bearer initial-access-token' + ); + scope.done(); + assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); }); // Allows a client to be instantiated from a certificate, diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts new file mode 100644 index 00000000..9543e8b7 --- /dev/null +++ b/test/test.identitypoolclient.ts @@ -0,0 +1,787 @@ +// Copyright 2021 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. + +import * as assert from 'assert'; +import {describe, it} from 'mocha'; +import * as fs from 'fs'; +import * as nock from 'nock'; + +import { + IdentityPoolClient, + IdentityPoolClientOptions, +} from '../src/auth/identitypoolclient'; +import {StsSuccessfulResponse} from '../src/auth/stscredentials'; +import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; +import { + assertGaxiosResponsePresent, + getAudience, + getTokenUrl, + getServiceAccountImpersonationUrl, + mockGenerateAccessToken, + mockStsTokenExchange, +} from './externalclienthelper'; + +nock.disableNetConnect(); + +const ONE_HOUR_IN_SECS = 3600; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping +function escapeRegExp(str: string): string { + // $& means the whole matched string. + return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); +} + +describe('IdentityPoolClient', () => { + const fileSubjectToken = fs.readFileSync( + './test/fixtures/external-subject-token.txt', + 'utf-8' + ); + const audience = getAudience(); + const fileSourcedOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: './test/fixtures/external-subject-token.txt', + }, + }; + const fileSourcedOptionsWithSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + fileSourcedOptions + ); + const jsonFileSourcedOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: './test/fixtures/external-subject-token.json', + format: { + type: 'json', + subject_token_field_name: 'access_token', + }, + }, + }; + const jsonFileSourcedOptionsWithSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + jsonFileSourcedOptions + ); + const fileSourcedOptionsNotFound = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: './test/fixtures/not-found', + }, + }; + const metadataBaseUrl = 'http://169.254.169.254'; + const metadataPath = + '/metadata/identity/oauth2/token?' + 'api-version=2018-02-01&resource=abc'; + const metadataHeaders = { + Metadata: 'True', + other: 'some-value', + }; + const urlSourcedOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + url: `${metadataBaseUrl}${metadataPath}`, + headers: metadataHeaders, + }, + }; + const urlSourcedOptionsWithSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + urlSourcedOptions + ); + const jsonRespUrlSourcedOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + url: `${metadataBaseUrl}${metadataPath}`, + headers: metadataHeaders, + format: { + type: 'json', + subject_token_field_name: 'access_token', + }, + }, + }; + const jsonRespUrlSourcedOptionsWithSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + jsonRespUrlSourcedOptions + ); + const stsSuccessfulResponse: StsSuccessfulResponse = { + access_token: 'ACCESS_TOKEN', + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: ONE_HOUR_IN_SECS, + scope: 'scope1 scope2', + }; + + it('should be a subclass of BaseExternalAccountClient', () => { + assert(IdentityPoolClient.prototype instanceof BaseExternalAccountClient); + }); + + describe('Constructor', () => { + it('should throw when invalid options are provided', () => { + const expectedError = new Error( + 'No valid Identity Pool "credential_source" provided' + ); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + other: 'invalid', + }, + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new IdentityPoolClient(invalidOptions as any); + }, expectedError); + }); + + it('should throw on invalid credential_source.format.type', () => { + const expectedError = new Error('Invalid credential_source format "xml"'); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: './test/fixtures/external-subject-token.txt', + format: { + type: 'xml', + }, + }, + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new IdentityPoolClient(invalidOptions as any); + }, expectedError); + }); + + it('should throw on required credential_source.format.subject_token_field_name', () => { + const expectedError = new Error( + 'Missing subject_token_field_name for JSON credential_source format' + ); + const invalidOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: './test/fixtures/external-subject-token.txt', + format: { + // json formats require the key where the subject_token is located. + type: 'json', + }, + }, + }; + + assert.throws(() => { + return new IdentityPoolClient(invalidOptions); + }, expectedError); + }); + + it('should not throw when valid file-sourced options are provided', () => { + assert.doesNotThrow(() => { + return new IdentityPoolClient(fileSourcedOptions); + }); + }); + + it('should not throw when valid url-sourced options are provided', () => { + assert.doesNotThrow(() => { + return new IdentityPoolClient(urlSourcedOptions); + }); + }); + + it('should not throw on headerless url-sourced options', () => { + const urlSourcedOptionsNoHeaders = Object.assign({}, urlSourcedOptions); + urlSourcedOptionsNoHeaders.credential_source = { + url: urlSourcedOptions.credential_source.url, + }; + assert.doesNotThrow(() => { + return new IdentityPoolClient(urlSourcedOptionsNoHeaders); + }); + }); + }); + + describe('for file-sourced subject tokens', () => { + describe('retrieveSubjectToken()', () => { + it('should resolve when the text file is found', async () => { + const client = new IdentityPoolClient(fileSourcedOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, fileSubjectToken); + }); + + it('should resolve when the json file is found', async () => { + const client = new IdentityPoolClient(jsonFileSourcedOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, fileSubjectToken); + }); + + it('should reject when the json subject_token_field_name is not found', async () => { + const expectedError = new Error( + 'Unable to parse the subject_token from the credential_source file' + ); + const invalidOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: './test/fixtures/external-subject-token.json', + format: { + type: 'json', + subject_token_field_name: 'non-existent', + }, + }, + }; + const client = new IdentityPoolClient(invalidOptions); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should fail when the file is not found', async () => { + const invalidFile = fileSourcedOptionsNotFound.credential_source.file; + const client = new IdentityPoolClient(fileSourcedOptionsNotFound); + + await assert.rejects( + client.retrieveSubjectToken(), + new RegExp( + `The file at ${escapeRegExp(invalidFile)} does not exist, ` + + 'or it is not a file' + ) + ); + }); + + it('should fail when a folder is specified', async () => { + const invalidOptions = Object.assign({}, fileSourcedOptions); + invalidOptions.credential_source = { + // Specify a folder. + file: './test/fixtures', + }; + const invalidFile = fs.realpathSync( + invalidOptions.credential_source.file + ); + const client = new IdentityPoolClient(invalidOptions); + + await assert.rejects( + client.retrieveSubjectToken(), + new RegExp( + `The file at ${escapeRegExp(invalidFile)} does not exist, ` + + 'or it is not a file' + ) + ); + }); + }); + + describe('getAccessToken()', () => { + it('should resolve on retrieveSubjectToken success for text format', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new IdentityPoolClient(fileSourcedOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should handle service account access token for text format', async () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new IdentityPoolClient(fileSourcedOptionsWithSA); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should resolve on retrieveSubjectToken success for json format', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new IdentityPoolClient(jsonFileSourcedOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should handle service account access token for json format', async () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new IdentityPoolClient(jsonFileSourcedOptionsWithSA); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should reject with retrieveSubjectToken error', async () => { + const invalidFile = fileSourcedOptionsNotFound.credential_source.file; + const client = new IdentityPoolClient(fileSourcedOptionsNotFound); + + await assert.rejects( + client.getAccessToken(), + new RegExp( + `The file at ${invalidFile} does not exist, or it is not a file` + ) + ); + }); + }); + }); + + describe('for url-sourced subject tokens', () => { + describe('retrieveSubjectToken()', () => { + it('should resolve on text response success', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scope = nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(200, externalSubjectToken); + + const client = new IdentityPoolClient(urlSourcedOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, externalSubjectToken); + scope.done(); + }); + + it('should resolve on json response success', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const jsonResponse = { + access_token: externalSubjectToken, + }; + const scope = nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(200, jsonResponse); + + const client = new IdentityPoolClient(jsonRespUrlSourcedOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, externalSubjectToken); + scope.done(); + }); + + it('should reject when the json subject_token_field_name is not found', async () => { + const expectedError = new Error( + 'Unable to parse the subject_token from the credential_source URL' + ); + const invalidOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + url: `${metadataBaseUrl}${metadataPath}`, + headers: metadataHeaders, + format: { + type: 'json', + subject_token_field_name: 'non-existent', + }, + }, + }; + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const jsonResponse = { + access_token: externalSubjectToken, + }; + const scope = nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(200, jsonResponse); + const client = new IdentityPoolClient(invalidOptions); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + scope.done(); + }); + + it('should ignore headers when not provided', async () => { + // Create options without headers. + const urlSourcedOptionsNoHeaders = Object.assign({}, urlSourcedOptions); + urlSourcedOptionsNoHeaders.credential_source = { + url: urlSourcedOptions.credential_source.url, + }; + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scope = nock(metadataBaseUrl) + .get(metadataPath) + .reply(200, externalSubjectToken); + + const client = new IdentityPoolClient(urlSourcedOptionsNoHeaders); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, externalSubjectToken); + scope.done(); + }); + + it('should reject with underlying on non-200 response', async () => { + const scope = nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(404); + + const client = new IdentityPoolClient(urlSourcedOptions); + + await assert.rejects(client.retrieveSubjectToken(), { + code: '404', + }); + scope.done(); + }); + }); + + describe('getAccessToken()', () => { + it('should resolve on retrieveSubjectToken success for text format', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token retrieved from url should be used. + subject_token: externalSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(200, externalSubjectToken) + ); + + const client = new IdentityPoolClient(urlSourcedOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should handle service account access token for text format', async () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(200, externalSubjectToken), + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token retrieved from url should be used. + subject_token: externalSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new IdentityPoolClient(urlSourcedOptionsWithSA); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should resolve on retrieveSubjectToken success for json format', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const jsonResponse = { + access_token: externalSubjectToken, + }; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token retrieved from url should be used. + subject_token: externalSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(200, jsonResponse) + ); + + const client = new IdentityPoolClient(jsonRespUrlSourcedOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should handle service account access token for json format', async () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const jsonResponse = { + access_token: externalSubjectToken, + }; + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(200, jsonResponse), + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token retrieved from url should be used. + subject_token: externalSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new IdentityPoolClient(jsonRespUrlSourcedOptionsWithSA); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should reject with retrieveSubjectToken error', async () => { + const scope = nock(metadataBaseUrl) + .get(metadataPath, undefined, { + reqheaders: metadataHeaders, + }) + .reply(404); + + const client = new IdentityPoolClient(urlSourcedOptions); + + await assert.rejects(client.getAccessToken(), { + code: '404', + }); + scope.done(); + }); + }); + }); +}); diff --git a/test/test.index.ts b/test/test.index.ts index a68e00f3..f790a3ca 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -38,5 +38,8 @@ describe('index', () => { assert(gal.OAuth2Client); assert(gal.UserRefreshClient); assert(gal.GoogleAuth); + assert(gal.ExternalAccountClient); + assert(gal.IdentityPoolClient); + assert(gal.AwsClient); }); }); diff --git a/test/test.oauth2common.ts b/test/test.oauth2common.ts new file mode 100644 index 00000000..5f57649b --- /dev/null +++ b/test/test.oauth2common.ts @@ -0,0 +1,459 @@ +// Copyright 2021 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. + +import {GaxiosOptions} from 'gaxios'; +import {describe, it} from 'mocha'; +import * as assert from 'assert'; +import * as querystring from 'querystring'; + +import {Headers} from '../src/auth/oauth2client'; +import { + ClientAuthentication, + OAuthClientAuthHandler, + getErrorFromOAuthErrorResponse, +} from '../src/auth/oauth2common'; + +/** Test class to test abstract class OAuthClientAuthHandler. */ +class TestOAuthClientAuthHandler extends OAuthClientAuthHandler { + testApplyClientAuthenticationOptions( + opts: GaxiosOptions, + bearerToken?: string + ) { + return this.applyClientAuthenticationOptions(opts, bearerToken); + } +} + +/** Custom error object for testing additional fields on an Error. */ +class CustomError extends Error { + public readonly code?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(message: string, stack?: any, code?: string) { + super(message); + this.name = 'CustomError'; + this.stack = stack; + this.code = code; + } +} + +describe('OAuthClientAuthHandler', () => { + const basicAuth: ClientAuthentication = { + confidentialClientType: 'basic', + clientId: 'username', + clientSecret: 'password', + }; + // Base64 encoding of "username:password" + const expectedBase64EncodedCred = 'dXNlcm5hbWU6cGFzc3dvcmQ='; + const basicAuthNoSecret: ClientAuthentication = { + confidentialClientType: 'basic', + clientId: 'username', + }; + // Base64 encoding of "username:" + const expectedBase64EncodedCredNoSecret = 'dXNlcm5hbWU6'; + const reqBodyAuth: ClientAuthentication = { + confidentialClientType: 'request-body', + clientId: 'username', + clientSecret: 'password', + }; + const reqBodyAuthNoSecret: ClientAuthentication = { + confidentialClientType: 'request-body', + clientId: 'username', + }; + + it('should not process request when no client authentication is used', () => { + const handler = new TestOAuthClientAuthHandler(); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + key1: 'value1', + key2: 'value2', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(originalOptions, actualOptions); + }); + + it('should process request with basic client auth', () => { + const handler = new TestOAuthClientAuthHandler(basicAuth); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + key1: 'value1', + key2: 'value2', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + (expectedOptions.headers as Headers).Authorization = `Basic ${expectedBase64EncodedCred}`; + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should process request with secretless basic client auth', () => { + const handler = new TestOAuthClientAuthHandler(basicAuthNoSecret); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + key1: 'value1', + key2: 'value2', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + (expectedOptions.headers as Headers).Authorization = `Basic ${expectedBase64EncodedCredNoSecret}`; + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should process GET (non-request-body) with basic client auth', () => { + const handler = new TestOAuthClientAuthHandler(basicAuth); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + (expectedOptions.headers as Headers).Authorization = `Basic ${expectedBase64EncodedCred}`; + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + describe('with request-body client auth', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const unsupportedMethods: any[] = [ + undefined, + 'GET', + 'DELETE', + 'TRACE', + 'OPTIONS', + 'HEAD', + ]; + unsupportedMethods.forEach(method => { + it(`should throw on requests with unsupported HTTP method ${method}`, () => { + const expectedError = new Error( + `${method || 'GET'} HTTP method does not support request-body ` + + 'client authentication' + ); + const handler = new TestOAuthClientAuthHandler(reqBodyAuth); + const originalOptions: GaxiosOptions = { + method, + url: 'https://www.example.com/path/to/api', + }; + + assert.throws(() => { + handler.testApplyClientAuthenticationOptions(originalOptions); + }, expectedError); + }); + }); + + it('should throw on unsupported content-types', () => { + const expectedError = new Error( + 'text/html content-types are not supported with request-body ' + + 'client authentication' + ); + const handler = new TestOAuthClientAuthHandler(reqBodyAuth); + const originalOptions: GaxiosOptions = { + headers: { + 'Content-Type': 'text/html', + }, + method: 'POST', + url: 'https://www.example.com/path/to/api', + }; + + assert.throws(() => { + handler.testApplyClientAuthenticationOptions(originalOptions); + }, expectedError); + }); + + it('should inject creds in non-empty json content', () => { + const handler = new TestOAuthClientAuthHandler(reqBodyAuth); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + key1: 'value1', + key2: 'value2', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + expectedOptions.data.client_id = reqBodyAuth.clientId; + expectedOptions.data.client_secret = reqBodyAuth.clientSecret; + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should inject secretless creds in json content', () => { + const handler = new TestOAuthClientAuthHandler(reqBodyAuthNoSecret); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + key1: 'value1', + key2: 'value2', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + expectedOptions.data.client_id = reqBodyAuthNoSecret.clientId; + expectedOptions.data.client_secret = ''; + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should inject creds in empty json content', () => { + const handler = new TestOAuthClientAuthHandler(reqBodyAuth); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + expectedOptions.data = { + client_id: reqBodyAuth.clientId, + client_secret: reqBodyAuth.clientSecret, + }; + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should inject creds in non-empty x-www-form-urlencoded content', () => { + const handler = new TestOAuthClientAuthHandler(reqBodyAuth); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + // Handling of headers should be case insensitive. + 'content-Type': 'application/x-www-form-urlencoded', + }, + data: querystring.stringify({key1: 'value1', key2: 'value2'}), + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + expectedOptions.data = querystring.stringify({ + key1: 'value1', + key2: 'value2', + client_id: reqBodyAuth.clientId, + client_secret: reqBodyAuth.clientSecret, + }); + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should inject secretless creds in x-www-form-urlencoded content', () => { + const handler = new TestOAuthClientAuthHandler(reqBodyAuthNoSecret); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: querystring.stringify({key1: 'value1', key2: 'value2'}), + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + expectedOptions.data = querystring.stringify({ + key1: 'value1', + key2: 'value2', + client_id: reqBodyAuth.clientId, + client_secret: '', + }); + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should inject creds in empty x-www-form-urlencoded content', () => { + const handler = new TestOAuthClientAuthHandler(reqBodyAuth); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + expectedOptions.data = querystring.stringify({ + client_id: reqBodyAuth.clientId, + client_secret: reqBodyAuth.clientSecret, + }); + + handler.testApplyClientAuthenticationOptions(actualOptions); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + }); + + it('should process request with bearer token when provided', () => { + const bearerToken = 'BEARER_TOKEN'; + const handler = new TestOAuthClientAuthHandler(); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + key1: 'value1', + key2: 'value2', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + const expectedOptions = Object.assign({}, originalOptions); + (expectedOptions.headers as Headers).Authorization = `Bearer ${bearerToken}`; + + handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should prioritize bearer token over basic auth', () => { + const bearerToken = 'BEARER_TOKEN'; + const handler = new TestOAuthClientAuthHandler(basicAuth); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + key1: 'value1', + key2: 'value2', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + // Expected options should have bearer token in header. + const expectedOptions = Object.assign({}, originalOptions); + (expectedOptions.headers as Headers).Authorization = `Bearer ${bearerToken}`; + + handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); + + it('should prioritize bearer token over request body', () => { + const bearerToken = 'BEARER_TOKEN'; + const handler = new TestOAuthClientAuthHandler(reqBodyAuth); + const originalOptions: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + key1: 'value1', + key2: 'value2', + }, + }; + const actualOptions = Object.assign({}, originalOptions); + // Expected options should have bearer token in header. + const expectedOptions = Object.assign({}, originalOptions); + (expectedOptions.headers as Headers).Authorization = `Bearer ${bearerToken}`; + + handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); + assert.deepStrictEqual(expectedOptions, actualOptions); + }); +}); + +describe('getErrorFromOAuthErrorResponse', () => { + it('should create expected error with code, description and uri', () => { + const resp = { + error: 'unsupported_grant_type', + error_description: 'The provided grant_type is unsupported', + error_uri: 'https://tools.ietf.org/html/rfc6749', + }; + const error = getErrorFromOAuthErrorResponse(resp); + assert.strictEqual( + error.message, + `Error code ${resp.error}: ${resp.error_description} ` + + `- ${resp.error_uri}` + ); + }); + + it('should create expected error with code and description', () => { + const resp = { + error: 'unsupported_grant_type', + error_description: 'The provided grant_type is unsupported', + }; + const error = getErrorFromOAuthErrorResponse(resp); + assert.strictEqual( + error.message, + `Error code ${resp.error}: ${resp.error_description}` + ); + }); + + it('should create expected error with code only', () => { + const resp = { + error: 'unsupported_grant_type', + }; + const error = getErrorFromOAuthErrorResponse(resp); + assert.strictEqual(error.message, `Error code ${resp.error}`); + }); + + it('should preserve the original error properties', () => { + const originalError = new CustomError( + 'Original error message', + 'Error stack', + '123456' + ); + const resp = { + error: 'unsupported_grant_type', + error_description: 'The provided grant_type is unsupported', + error_uri: 'https://tools.ietf.org/html/rfc6749', + }; + const expectedError = new CustomError( + `Error code ${resp.error}: ${resp.error_description} ` + + `- ${resp.error_uri}`, + 'Error stack', + '123456' + ); + + const actualError = getErrorFromOAuthErrorResponse(resp, originalError); + assert.strictEqual(actualError.message, expectedError.message); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assert.strictEqual((actualError as any).code, expectedError.code); + assert.strictEqual(actualError.name, expectedError.name); + assert.strictEqual(actualError.stack, expectedError.stack); + }); +}); diff --git a/test/test.stscredentials.ts b/test/test.stscredentials.ts new file mode 100644 index 00000000..72e17119 --- /dev/null +++ b/test/test.stscredentials.ts @@ -0,0 +1,403 @@ +// Copyright 2021 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. + +import * as assert from 'assert'; +import {describe, it, afterEach} from 'mocha'; +import * as qs from 'querystring'; +import * as nock from 'nock'; +import {createCrypto} from '../src/crypto/crypto'; +import { + StsCredentials, + StsCredentialsOptions, + StsSuccessfulResponse, +} from '../src/auth/stscredentials'; +import { + ClientAuthentication, + OAuthErrorResponse, + getErrorFromOAuthErrorResponse, +} from '../src/auth/oauth2common'; + +nock.disableNetConnect(); + +describe('StsCredentials', () => { + const crypto = createCrypto(); + const baseUrl = 'https://example.com'; + const path = '/token.oauth2'; + const tokenExchangeEndpoint = `${baseUrl}${path}`; + const basicAuth: ClientAuthentication = { + confidentialClientType: 'basic', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + }; + const requestBodyAuth: ClientAuthentication = { + confidentialClientType: 'request-body', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + }; + // Full STS credentials options, useful to test that all supported + // parameters are handled correctly. + const stsCredentialsOptions: StsCredentialsOptions = { + grantType: 'urn:ietf:params:oauth:grant-type:token-exchange', + resource: 'https://api.example.com/', + audience: 'urn:example:cooperation-context', + scope: ['scope1', 'scope2'], + requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token', + subjectToken: 'HEADER.SUBJECT_TOKEN_PAYLOAD.SIGNATURE', + subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt', + actingParty: { + actorToken: 'HEADER.ACTOR_TOKEN_PAYLOAD.SIGNATURE', + actorTokenType: 'urn:ietf:params:oauth:token-type:jwt', + }, + }; + // Partial STS credentials options, useful to test that optional unspecified + // parameters are handled correctly. + const partialStsCredentialsOptions: StsCredentialsOptions = { + grantType: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: 'urn:example:cooperation-context', + requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token', + subjectToken: 'HEADER.SUBJECT_TOKEN_PAYLOAD.SIGNATURE', + subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt', + }; + const stsSuccessfulResponse: StsSuccessfulResponse = { + access_token: 'ACCESS_TOKEN', + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: 3600, + scope: 'scope1 scope2', + }; + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + + function assertGaxiosResponsePresent(resp: StsSuccessfulResponse) { + const gaxiosResponse = resp.res || {}; + assert('data' in gaxiosResponse && 'status' in gaxiosResponse); + } + + function mockStsTokenExchange( + statusCode = 200, + response: StsSuccessfulResponse | OAuthErrorResponse, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: {[key: string]: any}, + additionalHeaders?: {[key: string]: string} + ): nock.Scope { + const headers = Object.assign( + { + 'content-type': 'application/x-www-form-urlencoded', + }, + additionalHeaders || {} + ); + return nock(baseUrl) + .post(path, qs.stringify(request), { + reqheaders: headers, + }) + .reply(statusCode, response); + } + + afterEach(() => { + nock.cleanAll(); + }); + + describe('exchangeToken()', () => { + const additionalHeaders = { + 'x-client-version': '0.1.2', + }; + const options = { + additional: { + 'non-standard': ['options'], + other: 'some-value', + }, + }; + const expectedRequest = { + grant_type: stsCredentialsOptions.grantType, + resource: stsCredentialsOptions.resource, + audience: stsCredentialsOptions.audience, + scope: stsCredentialsOptions.scope?.join(' '), + requested_token_type: stsCredentialsOptions.requestedTokenType, + subject_token: stsCredentialsOptions.subjectToken, + subject_token_type: stsCredentialsOptions.subjectTokenType, + actor_token: stsCredentialsOptions.actingParty?.actorToken, + actor_token_type: stsCredentialsOptions.actingParty?.actorTokenType, + options: JSON.stringify(options), + }; + const expectedPartialRequest = { + grant_type: stsCredentialsOptions.grantType, + audience: stsCredentialsOptions.audience, + requested_token_type: stsCredentialsOptions.requestedTokenType, + subject_token: stsCredentialsOptions.subjectToken, + subject_token_type: stsCredentialsOptions.subjectTokenType, + }; + const expectedRequestWithCreds = Object.assign({}, expectedRequest, { + client_id: requestBodyAuth.clientId, + client_secret: requestBodyAuth.clientSecret, + }); + const expectedPartialRequestWithCreds = Object.assign( + {}, + expectedPartialRequest, + { + client_id: requestBodyAuth.clientId, + client_secret: requestBodyAuth.clientSecret, + } + ); + + describe('without client authentication', () => { + it('should handle successful full request', async () => { + const scope = mockStsTokenExchange( + 200, + stsSuccessfulResponse, + expectedRequest, + additionalHeaders + ); + const stsCredentials = new StsCredentials(tokenExchangeEndpoint); + + const resp = await stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options + ); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(resp); + delete resp.res; + assert.deepStrictEqual(resp, stsSuccessfulResponse); + scope.done(); + }); + + it('should handle successful partial request', async () => { + const scope = mockStsTokenExchange( + 200, + stsSuccessfulResponse, + expectedPartialRequest + ); + const stsCredentials = new StsCredentials(tokenExchangeEndpoint); + + const resp = await stsCredentials.exchangeToken( + partialStsCredentialsOptions + ); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(resp); + delete resp.res; + assert.deepStrictEqual(resp, stsSuccessfulResponse); + scope.done(); + }); + + it('should handle non-200 response', async () => { + const scope = mockStsTokenExchange( + 400, + errorResponse, + expectedRequest, + additionalHeaders + ); + const expectedError = getErrorFromOAuthErrorResponse(errorResponse); + const stsCredentials = new StsCredentials(tokenExchangeEndpoint); + + await assert.rejects( + stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options + ), + expectedError + ); + scope.done(); + }); + + it('should handle request timeout', async () => { + const scope = nock(baseUrl) + .post(path, qs.stringify(expectedRequest), { + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }) + .replyWithError({code: 'ETIMEDOUT'}); + const stsCredentials = new StsCredentials(tokenExchangeEndpoint); + + await assert.rejects( + stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options + ), + { + code: 'ETIMEDOUT', + } + ); + scope.done(); + }); + }); + + describe('with basic client authentication', () => { + const creds = `${basicAuth.clientId}:${basicAuth.clientSecret}`; + it('should handle successful full request', async () => { + const scope = mockStsTokenExchange( + 200, + stsSuccessfulResponse, + expectedRequest, + Object.assign( + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, + }, + additionalHeaders + ) + ); + const stsCredentials = new StsCredentials( + tokenExchangeEndpoint, + basicAuth + ); + + const resp = await stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options + ); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(resp); + delete resp.res; + assert.deepStrictEqual(resp, stsSuccessfulResponse); + scope.done(); + }); + + it('should handle successful partial request', async () => { + const scope = mockStsTokenExchange( + 200, + stsSuccessfulResponse, + expectedPartialRequest, + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, + } + ); + const stsCredentials = new StsCredentials( + tokenExchangeEndpoint, + basicAuth + ); + + const resp = await stsCredentials.exchangeToken( + partialStsCredentialsOptions + ); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(resp); + delete resp.res; + assert.deepStrictEqual(resp, stsSuccessfulResponse); + scope.done(); + }); + + it('should handle non-200 response', async () => { + const expectedError = getErrorFromOAuthErrorResponse(errorResponse); + const scope = mockStsTokenExchange( + 400, + errorResponse, + expectedRequest, + Object.assign( + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, + }, + additionalHeaders + ) + ); + const stsCredentials = new StsCredentials( + tokenExchangeEndpoint, + basicAuth + ); + + await assert.rejects( + stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options + ), + expectedError + ); + scope.done(); + }); + }); + + describe('with request-body client authentication', () => { + it('should handle successful full request', async () => { + const scope = mockStsTokenExchange( + 200, + stsSuccessfulResponse, + expectedRequestWithCreds, + additionalHeaders + ); + const stsCredentials = new StsCredentials( + tokenExchangeEndpoint, + requestBodyAuth + ); + + const resp = await stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options + ); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(resp); + delete resp.res; + assert.deepStrictEqual(resp, stsSuccessfulResponse); + scope.done(); + }); + + it('should handle successful partial request', async () => { + const scope = mockStsTokenExchange( + 200, + stsSuccessfulResponse, + expectedPartialRequestWithCreds + ); + const stsCredentials = new StsCredentials( + tokenExchangeEndpoint, + requestBodyAuth + ); + + const resp = await stsCredentials.exchangeToken( + partialStsCredentialsOptions + ); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(resp); + delete resp.res; + assert.deepStrictEqual(resp, stsSuccessfulResponse); + scope.done(); + }); + + it('should handle non-200 response', async () => { + const expectedError = getErrorFromOAuthErrorResponse(errorResponse); + const scope = mockStsTokenExchange( + 400, + errorResponse, + expectedRequestWithCreds, + additionalHeaders + ); + const stsCredentials = new StsCredentials( + tokenExchangeEndpoint, + requestBodyAuth + ); + + await assert.rejects( + stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options + ), + expectedError + ); + scope.done(); + }); + }); + }); +}); From 80721ffe6f9d7732e0f621153fbd0747c249dac9 Mon Sep 17 00:00:00 2001 From: Steve Mao Date: Tue, 9 Feb 2021 05:23:27 +1100 Subject: [PATCH 220/662] docs: fix typo in jsdoc --- samples/credentials.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/credentials.js b/samples/credentials.js index acbefac6..5791490e 100644 --- a/samples/credentials.js +++ b/samples/credentials.js @@ -20,7 +20,7 @@ const {GoogleAuth} = require('google-auth-library'); /** * This sample demonstrates passing a `credentials` object directly into the - * `getClient` method. This is useful if you're storing the fields requiretd + * `getClient` method. This is useful if you're storing the fields required * in environment variables. The original `client_email` and `private_key` * values are obtained from a service account credential file. */ From bff0619840bbe063b923fad06422d1d096e19e54 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 8 Feb 2021 11:00:50 -0800 Subject: [PATCH 221/662] deps: update karma-webpack/webpack (#1138) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c4737d1e..4e9a3fa6 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "karma-mocha": "^2.0.0", "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^4.0.0", + "karma-webpack": "^5.0.0", "keypair": "^1.0.1", "linkinator": "^2.0.0", "mocha": "^8.0.0", @@ -68,7 +68,7 @@ "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "^3.8.3", - "webpack": "^4.20.2", + "webpack": "^5.21.2", "webpack-cli": "^4.0.0" }, "files": [ From 309dab39813b4adf1042375e2461486f9e9a3df4 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 8 Feb 2021 11:25:36 -0800 Subject: [PATCH 222/662] docs: add workload identity content to partials (#1139) Co-authored-by: Justin Beckwith --- .readme-partials.yaml | 212 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 211 insertions(+), 1 deletion(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index ddcf3b21..5472a003 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -3,16 +3,19 @@ introduction: |- body: |- ## Ways to authenticate This library provides a variety of ways to authenticate to your Google services. - - [Application Default Credentials](#choosing-the-correct-credential-type-automatically) - Use Application Default Credentials when you use a single identity for all users in your application. Especially useful for applications running on Google Cloud. + - [Application Default Credentials](#choosing-the-correct-credential-type-automatically) - Use Application Default Credentials when you use a single identity for all users in your application. Especially useful for applications running on Google Cloud. Application Default Credentials also support workload identity federation to access Google Cloud resources from non-Google Cloud platforms. - [OAuth 2](#oauth2) - Use OAuth2 when you need to perform actions on behalf of the end user. - [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication. - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. + - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). ## Application Default Credentials This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. + Application Default Credentials also support workload identity federation to access Google Cloud resources from non-Google Cloud platforms including Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). Workload identity federation is recommended for non-Google Cloud environments as it avoids the need to download, manage and store service account private keys locally, see: [Workload Identity Federation](#workload-identity-federation). + #### Download your Service Account Credentials JSON file To use Application Default Credentials, You first need to download a set of JSON credentials for your project. Go to **APIs & Auth** > **Credentials** in the [Google Developers Console](https://console.cloud.google.com/) and select **Service account** from the **Add credentials** dropdown. @@ -318,6 +321,213 @@ body: |- main().catch(console.error); ``` + ## Workload Identity Federation + + Using workload identity federation, your application can access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). + + Traditionally, applications running outside Google Cloud have used service account keys to access Google Cloud resources. Using identity federation, you can allow your workload to impersonate a service account. + This lets you access Google Cloud resources directly, eliminating the maintenance and security burden associated with service account keys. + + ### Accessing resources from AWS + + In order to access Google Cloud resources from Amazon Web Services (AWS), the following requirements are needed: + - A workload identity pool needs to be created. + - AWS needs to be added as an identity provider in the workload identity pool (The Google [organization policy](https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict) needs to allow federation from AWS). + - Permission to impersonate a service account needs to be granted to the external identity. + + Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-resources-aws) on how to configure workload identity federation from AWS. + + After configuring the AWS provider to impersonate a service account, a credential configuration file needs to be generated. + Unlike service account credential files, the generated credential configuration file will only contain non-sensitive metadata to instruct the library on how to retrieve external subject tokens and exchange them for service account access tokens. + The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). + + To generate the AWS workload identity configuration, run the following command: + + ```bash + # Generate an AWS configuration file. + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AWS_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --aws \ + --output-file /path/to/generated/config.json + ``` + + Where the following variables need to be substituted: + - `$PROJECT_NUMBER`: The Google Cloud project number. + - `$POOL_ID`: The workload identity pool ID. + - `$AWS_PROVIDER_ID`: The AWS provider ID. + - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + + This will generate the configuration file in the specified output file. + + You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from AWS. + + ### Access resources from Microsoft Azure + + In order to access Google Cloud resources from Microsoft Azure, the following requirements are needed: + - A workload identity pool needs to be created. + - Azure needs to be added as an identity provider in the workload identity pool (The Google [organization policy](https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict) needs to allow federation from Azure). + - The Azure tenant needs to be configured for identity federation. + - Permission to impersonate a service account needs to be granted to the external identity. + + Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-resources-azure) on how to configure workload identity federation from Microsoft Azure. + + After configuring the Azure provider to impersonate a service account, a credential configuration file needs to be generated. + Unlike service account credential files, the generated credential configuration file will only contain non-sensitive metadata to instruct the library on how to retrieve external subject tokens and exchange them for service account access tokens. + The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). + + To generate the Azure workload identity configuration, run the following command: + + ```bash + # Generate an Azure configuration file. + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AZURE_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --azure \ + --output-file /path/to/generated/config.json + ``` + + Where the following variables need to be substituted: + - `$PROJECT_NUMBER`: The Google Cloud project number. + - `$POOL_ID`: The workload identity pool ID. + - `$AZURE_PROVIDER_ID`: The Azure provider ID. + - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + + This will generate the configuration file in the specified output file. + + You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from Azure. + + ### Accessing resources from an OIDC identity provider + + In order to access Google Cloud resources from an identity provider that supports [OpenID Connect (OIDC)](https://openid.net/connect/), the following requirements are needed: + - A workload identity pool needs to be created. + - An OIDC identity provider needs to be added in the workload identity pool (The Google [organization policy](https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict) needs to allow federation from the identity provider). + - Permission to impersonate a service account needs to be granted to the external identity. + + Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-resources-oidc) on how to configure workload identity federation from an OIDC identity provider. + + After configuring the OIDC provider to impersonate a service account, a credential configuration file needs to be generated. + Unlike service account credential files, the generated credential configuration file will only contain non-sensitive metadata to instruct the library on how to retrieve external subject tokens and exchange them for service account access tokens. + The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). + + For OIDC providers, the Auth library can retrieve OIDC tokens either from a local file location (file-sourced credentials) or from a local server (URL-sourced credentials). + + **File-sourced credentials** + For file-sourced credentials, a background process needs to be continuously refreshing the file location with a new OIDC token prior to expiration. + For tokens with one hour lifetimes, the token needs to be updated in the file every hour. The token can be stored directly as plain text or in JSON format. + + To generate a file-sourced OIDC configuration, run the following command: + + ```bash + # Generate an OIDC configuration file for file-sourced credentials. + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$OIDC_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --credential-source-file $PATH_TO_OIDC_ID_TOKEN \ + # Optional arguments for file types. Default is "text": + # --credential-source-type "json" \ + # Optional argument for the field that contains the OIDC credential. + # This is required for json. + # --credential-source-field-name "id_token" \ + --output-file /path/to/generated/config.json + ``` + + Where the following variables need to be substituted: + - `$PROJECT_NUMBER`: The Google Cloud project number. + - `$POOL_ID`: The workload identity pool ID. + - `$OIDC_PROVIDER_ID`: The OIDC provider ID. + - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + - `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from. + + This will generate the configuration file in the specified output file. + + **URL-sourced credentials** + For URL-sourced credentials, a local server needs to host a GET endpoint to return the OIDC token. The response can be in plain text or JSON. + Additional required request headers can also be specified. + + To generate a URL-sourced OIDC workload identity configuration, run the following command: + + ```bash + # Generate an OIDC configuration file for URL-sourced credentials. + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$OIDC_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --credential-source-url $URL_TO_GET_OIDC_TOKEN \ + --credential-source-headers $HEADER_KEY=$HEADER_VALUE \ + # Optional arguments for file types. Default is "text": + # --credential-source-type "json" \ + # Optional argument for the field that contains the OIDC credential. + # This is required for json. + # --credential-source-field-name "id_token" \ + --output-file /path/to/generated/config.json + ``` + + Where the following variables need to be substituted: + - `$PROJECT_NUMBER`: The Google Cloud project number. + - `$POOL_ID`: The workload identity pool ID. + - `$OIDC_PROVIDER_ID`: The OIDC provider ID. + - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. + - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. + + You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC provider. + + ### Using External Identities + + External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. + In order to use external identities with Application Default Credentials, you need to generate the JSON credentials configuration file for your external identity as described above. + Once generated, store the path to this file in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. + + ```bash + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/config.json + ``` + + The library can now automatically choose the right type of client and initialize credentials from the context provided in the configuration file. + + ```js + async function main() { + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform' + }); + const client = await auth.getClient(); + const projectId = await auth.getProjectId(); + // List all buckets in a project. + const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; + const res = await client.request({ url }); + console.log(res.data); + } + ``` + + When using external identities with Application Default Credentials in Node.js, the `roles/browser` role needs to be granted to the service account. + The `Cloud Resource Manager API` should also be enabled on the project. + This is needed since the library will try to auto-discover the project ID from the current environment using the impersonated credential. + To avoid this requirement, the project ID can be explicitly specified on initialization. + + ```js + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + // Pass the project ID explicitly to avoid the need to grant `roles/browser` to the service account + // or enable Cloud Resource Manager API on the project. + projectId: 'CLOUD_RESOURCE_PROJECT_ID', + }); + ``` + + You can also explicitly initialize external account clients using the generated configuration file. + + ```js + const {ExternalAccountClient} = require('google-auth-library'); + const jsonConfig = require('/path/to/config.json'); + + async function main() { + const client = ExternalAccountClient.fromJSON(jsonConfig); + client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; + // List all buckets in a project. + const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; + const res = await client.request({url}); + console.log(res.data); + } + ``` + ## Working with ID Tokens ### Fetching ID Tokens If your application is running on Cloud Run or Cloud Functions, or using Cloud Identity-Aware From 04161ba00a6ce4ddc93b942cea2869e88b0e62de Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 11:38:53 -0800 Subject: [PATCH 223/662] chore: release 7.0.0 (#1135) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Benjamin E. Coe --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc0dbcb..533c21c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.0.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.6...v7.0.0) (2021-02-08) + + +### ⚠ BREAKING CHANGES + +* integrates external_accounts with `GoogleAuth` and ADC (#1052) +* workload identity federation support (#1131) + +### Features + +* adds service account impersonation to `ExternalAccountClient` ([#1041](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1041)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* adds text/json credential_source support to IdentityPoolClients ([#1059](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1059)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* defines `ExternalAccountClient` used to instantiate external account clients ([#1050](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1050)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* defines `IdentityPoolClient` used for K8s and Azure workloads ([#1042](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1042)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* defines ExternalAccountClient abstract class for external_account credentials ([#1030](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1030)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* get AWS region from environment variable ([#1067](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1067)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* implements AWS signature version 4 for signing requests ([#1047](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1047)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* implements the OAuth token exchange spec based on rfc8693 ([#1026](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1026)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* integrates external_accounts with `GoogleAuth` and ADC ([#1052](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1052)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) +* workload identity federation support ([#1131](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1131)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v6 ([#1129](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1129)) ([5240fb0](https://www.github.com/googleapis/google-auth-library-nodejs/commit/5240fb0e7ba5503d562659a0d1d7c952bc44ce0e)) +* **deps:** update dependency puppeteer to v7 ([#1134](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1134)) ([02d0d73](https://www.github.com/googleapis/google-auth-library-nodejs/commit/02d0d73a5f0d2fc7de9b13b160e4e7074652f9d0)) + ### [6.1.6](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.5...v6.1.6) (2021-01-27) diff --git a/package.json b/package.json index 4e9a3fa6..5876b03d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "6.1.6", + "version": "7.0.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index b9b5064d..95f2bac0 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.1.6", + "google-auth-library": "^7.0.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 9c717f70ca155b24edd5511b6038679db25b85b7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 9 Feb 2021 19:22:04 +0100 Subject: [PATCH 224/662] fix(deps): update dependency google-auth-library to v7 (#1140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-auth-library](https://togithub.com/googleapis/google-auth-library-nodejs) | [`^6.0.0` -> `^7.0.0`](https://renovatebot.com/diffs/npm/google-auth-library/6.1.6/7.0.0) | [![age](https://badges.renovateapi.com/packages/npm/google-auth-library/7.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/google-auth-library/7.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/google-auth-library/7.0.0/compatibility-slim/6.1.6)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/google-auth-library/7.0.0/confidence-slim/6.1.6)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/google-auth-library-nodejs ### [`v7.0.0`](https://togithub.com/googleapis/google-auth-library-nodejs/blob/master/CHANGELOG.md#​700-httpswwwgithubcomgoogleapisgoogle-auth-library-nodejscomparev616v700-2021-02-08) [Compare Source](https://togithub.com/googleapis/google-auth-library-nodejs/compare/v6.1.6...v7.0.0) ##### ⚠ BREAKING CHANGES - integrates external_accounts with `GoogleAuth` and ADC ([#​1052](https://togithub.com/googleapis/google-auth-library-nodejs/issues/1052)) - workload identity federation support ([#​1131](https://togithub.com/googleapis/google-auth-library-nodejs/issues/1131)) ##### Features - adds service account impersonation to `ExternalAccountClient` ([#​1041](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1041)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - adds text/json credential_source support to IdentityPoolClients ([#​1059](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1059)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - defines `ExternalAccountClient` used to instantiate external account clients ([#​1050](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1050)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - defines `IdentityPoolClient` used for K8s and Azure workloads ([#​1042](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1042)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - defines ExternalAccountClient abstract class for external_account credentials ([#​1030](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1030)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - get AWS region from environment variable ([#​1067](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1067)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - implements AWS signature version 4 for signing requests ([#​1047](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1047)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - implements the OAuth token exchange spec based on rfc8693 ([#​1026](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1026)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - integrates external_accounts with `GoogleAuth` and ADC ([#​1052](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1052)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) - workload identity federation support ([#​1131](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1131)) ([997f124](https://www.github.com/googleapis/google-auth-library-nodejs/commit/997f124a5c02dfa44879a759bf701a9fa4c3ba90)) ##### Bug Fixes - **deps:** update dependency puppeteer to v6 ([#​1129](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1129)) ([5240fb0](https://www.github.com/googleapis/google-auth-library-nodejs/commit/5240fb0e7ba5503d562659a0d1d7c952bc44ce0e)) - **deps:** update dependency puppeteer to v7 ([#​1134](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1134)) ([02d0d73](https://www.github.com/googleapis/google-auth-library-nodejs/commit/02d0d73a5f0d2fc7de9b13b160e4e7074652f9d0)) ##### [6.1.6](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.5...v6.1.6) (2021-01-27) ##### Bug Fixes - call addSharedMetadataHeaders even when token has not expired ([#​1116](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1116)) ([aad043d](https://www.github.com/googleapis/google-auth-library-nodejs/commit/aad043d20df3f1e44f56c58a21f15000b6fe970d)) ##### [6.1.5](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.4...v6.1.5) (2021-01-22) ##### Bug Fixes - support PEM and p12 when using factory ([#​1120](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1120)) ([c2ead4c](https://www.github.com/googleapis/google-auth-library-nodejs/commit/c2ead4cc7650f100b883c9296fce628f17085992)) ##### [6.1.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.3...v6.1.4) (2020-12-22) ##### Bug Fixes - move accessToken to headers instead of parameter ([#​1108](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1108)) ([67b0cc3](https://www.github.com/googleapis/google-auth-library-nodejs/commit/67b0cc3077860a1583bcf18ce50aeff58bbb5496)) ##### [6.1.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.2...v6.1.3) (2020-10-22) ##### Bug Fixes - **deps:** update dependency gaxios to v4 ([#​1086](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1086)) ([f2678ff](https://www.github.com/googleapis/google-auth-library-nodejs/commit/f2678ff5f8f5a0ee33924278b58e0a6e3122cb12)) ##### [6.1.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.1...v6.1.2) (2020-10-19) ##### Bug Fixes - update gcp-metadata to catch a json-bigint security fix ([#​1078](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1078)) ([125fe09](https://www.github.com/googleapis/google-auth-library-nodejs/commit/125fe0924a2206ebb0c83ece9947524e7b135803)) ##### [6.1.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.0...v6.1.1) (2020-10-06) ##### Bug Fixes - **deps:** upgrade gtoken ([#​1064](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1064)) ([9116f24](https://www.github.com/googleapis/google-auth-library-nodejs/commit/9116f247486d6376feca505bbfa42a91d5e579e2))
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- samples/puppeteer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index db1b73ad..58ff4c69 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -11,7 +11,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^6.0.0", + "google-auth-library": "^7.0.0", "puppeteer": "^7.0.0" } } From 4c9da950b75f0ca3292c2f6bbb5a26e5031a7c6f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 9 Feb 2021 18:34:04 +0000 Subject: [PATCH 225/662] chore: release 7.0.1 (#1141) :robot: I have created a release \*beep\* \*boop\* --- ### [7.0.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.0...v7.0.1) (2021-02-09) ### Bug Fixes * **deps:** update dependency google-auth-library to v7 ([#1140](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1140)) ([9c717f7](https://www.github.com/googleapis/google-auth-library-nodejs/commit/9c717f70ca155b24edd5511b6038679db25b85b7)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 533c21c1..1bf3b31a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.0.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.0...v7.0.1) (2021-02-09) + + +### Bug Fixes + +* **deps:** update dependency google-auth-library to v7 ([#1140](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1140)) ([9c717f7](https://www.github.com/googleapis/google-auth-library-nodejs/commit/9c717f70ca155b24edd5511b6038679db25b85b7)) + ## [7.0.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v6.1.6...v7.0.0) (2021-02-08) diff --git a/package.json b/package.json index 5876b03d..e2dd894d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.0.0", + "version": "7.0.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 95f2bac0..86d401cd 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^7.0.0", + "google-auth-library": "^7.0.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 1d62c04dfa117b6a81e8c78385dc72792369cf21 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Wed, 10 Feb 2021 10:56:57 -0800 Subject: [PATCH 226/662] fix: expose `BaseExternalAccountClient` and `BaseExternalAccountClientOptions` (#1142) The above base class and interface need to be exposed to unblock downstream libraries dependency on v7. https://github.com/googleapis/nodejs-googleapis-common/pull/363 https://github.com/googleapis/google-api-nodejs-client/pull/2514 https://github.com/googleapis/nodejs-translate/pull/622 https://github.com/googleapis/gax-nodejs/pull/959 --- src/index.ts | 4 ++++ test/test.index.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/src/index.ts b/src/index.ts index 6e2f23b7..4642cf14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,10 @@ export { ExternalAccountClient, ExternalAccountClientOptions, } from './auth/externalclient'; +export { + BaseExternalAccountClient, + BaseExternalAccountClientOptions, +} from './auth/baseexternalclient'; export {DefaultTransporter} from './transporters'; const auth = new GoogleAuth(); diff --git a/test/test.index.ts b/test/test.index.ts index f790a3ca..06bb8190 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -41,5 +41,6 @@ describe('index', () => { assert(gal.ExternalAccountClient); assert(gal.IdentityPoolClient); assert(gal.AwsClient); + assert(gal.BaseExternalAccountClient); }); }); From e21cf6fa3ef8a520b680c3e480c1bd3e20516fb4 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 10 Feb 2021 11:09:33 -0800 Subject: [PATCH 227/662] chore: release 7.0.2 (#1143) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf3b31a..5612b2f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.0.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.1...v7.0.2) (2021-02-10) + + +### Bug Fixes + +* expose `BaseExternalAccountClient` and `BaseExternalAccountClientOptions` ([#1142](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1142)) ([1d62c04](https://www.github.com/googleapis/google-auth-library-nodejs/commit/1d62c04dfa117b6a81e8c78385dc72792369cf21)) + ### [7.0.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.0...v7.0.1) (2021-02-09) diff --git a/package.json b/package.json index e2dd894d..4debc912 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.0.1", + "version": "7.0.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 86d401cd..a430f21e 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^7.0.1", + "google-auth-library": "^7.0.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From fbad5d383be691099a7dab29dca27d5263c4dfb1 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 26 Feb 2021 16:54:34 +0100 Subject: [PATCH 228/662] chore(deps): update dependency puppeteer to v8 (#1146) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4debc912..37ebce36 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^7.0.0", + "puppeteer": "^8.0.0", "sinon": "^9.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 58ff4c69..95e71274 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^7.0.0", - "puppeteer": "^7.0.0" + "puppeteer": "^8.0.0" } } From bd6dee5fe1293046afcc8a97fab07a1e5953c34a Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Fri, 12 Mar 2021 14:47:01 -0800 Subject: [PATCH 229/662] build: remove blunderbuss config (#1147) --- .github/blunderbuss.yml | 6 ------ system-test/test.kitchen.ts | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .github/blunderbuss.yml diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml deleted file mode 100644 index 613fbf77..00000000 --- a/.github/blunderbuss.yml +++ /dev/null @@ -1,6 +0,0 @@ -assign_issues: - - sofisl - - bcoe -assign_prs: - - sofisl - - bcoe diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 630f9b82..dfd0a424 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); From 99cfba45864feef2c20b167f6c027d8d61bdd79c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 23 Mar 2021 17:50:03 +0100 Subject: [PATCH 230/662] chore(deps): update dependency sinon to v10 (#1150) [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [sinon](https://sinonjs.org/) ([source](https://togithub.com/sinonjs/sinon)) | [`^9.0.0` -> `^10.0.0`](https://renovatebot.com/diffs/npm/sinon/9.2.4/10.0.0) | [![age](https://badges.renovateapi.com/packages/npm/sinon/10.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/sinon/10.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/sinon/10.0.0/compatibility-slim/9.2.4)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/sinon/10.0.0/confidence-slim/9.2.4)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
sinonjs/sinon ### [`v10.0.0`](https://togithub.com/sinonjs/sinon/blob/master/CHANGELOG.md#​1000--2021-03-22) [Compare Source](https://togithub.com/sinonjs/sinon/compare/v9.2.4...v10.0.0) ================== - Upgrade nise to 4.1.0 - Use [@​sinonjs/eslint-config](https://togithub.com/sinonjs/eslint-config)[@​4](https://togithub.com/4) => Adopts ES2017 => Drops support for IE 11, Legacy Edge and legacy Safari
--- ### Renovate configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 37ebce36..498cadec 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "nock": "^13.0.0", "null-loader": "^4.0.0", "puppeteer": "^8.0.0", - "sinon": "^9.0.0", + "sinon": "^10.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "^3.8.3", From 9ae2d30c15c9bce3cae70ccbe6e227c096005695 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Tue, 23 Mar 2021 12:12:04 -0700 Subject: [PATCH 231/662] fix: support AWS_DEFAULT_REGION for determining AWS region (#1149) `AWS_DEFAULT_REGION` is also a supported environment variable for the AWS region: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html Priority order for region determination: `AWS_REGION` > `AWS_DEFAULT_REGION` > AWS metadata server --- src/auth/awsclient.ts | 11 +++++++---- test/test.awsclient.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index 6b873488..9995e2f7 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -27,7 +27,8 @@ import {RefreshOptions} from './oauth2client'; export interface AwsClientOptions extends BaseExternalAccountClientOptions { credential_source: { environment_id: string; - // Region can also be determined from the AWS_REGION environment variable. + // Region can also be determined from the AWS_REGION or AWS_DEFAULT_REGION + // environment variables. region_url?: string; // The url field is used to determine the AWS security credentials. // This is optional since these credentials can be retrieved from the @@ -78,7 +79,7 @@ export class AwsClient extends BaseExternalAccountClient { super(options, additionalOptions); this.environmentId = options.credential_source.environment_id; // This is only required if the AWS region is not available in the - // AWS_REGION environment variable + // AWS_REGION or AWS_DEFAULT_REGION environment variables. this.regionUrl = options.credential_source.region_url; // This is only required if AWS security credentials are not available in // environment variables. @@ -200,8 +201,10 @@ export class AwsClient extends BaseExternalAccountClient { * @return A promise that resolves with the current AWS region. */ private async getAwsRegion(): Promise { - if (process.env['AWS_REGION']) { - return process.env['AWS_REGION']; + // Priority order for region determination: + // AWS_REGION > AWS_DEFAULT_REGION > metadata server. + if (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION']) { + return (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'])!; } if (!this.regionUrl) { throw new Error( diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index b318cea2..9ce18920 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -527,18 +527,21 @@ describe('AwsClient', () => { let envAwsSecretAccessKey: string | undefined; let envAwsSessionToken: string | undefined; let envAwsRegion: string | undefined; + let envAwsDefaultRegion: string | undefined; beforeEach(() => { // Store external state. envAwsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; envAwsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; envAwsSessionToken = process.env.AWS_SESSION_TOKEN; - envAwsAccessKeyId = process.env.AWS_REGION; + envAwsRegion = process.env.AWS_REGION; + envAwsDefaultRegion = process.env.AWS_DEFAULT_REGION; // Reset environment variables. delete process.env.AWS_ACCESS_KEY_ID; delete process.env.AWS_SECRET_ACCESS_KEY; delete process.env.AWS_SESSION_TOKEN; delete process.env.AWS_REGION; + delete process.env.AWS_DEFAULT_REGION; }); afterEach(() => { @@ -563,6 +566,11 @@ describe('AwsClient', () => { } else { delete process.env.AWS_REGION; } + if (envAwsDefaultRegion) { + process.env.AWS_DEFAULT_REGION = envAwsDefaultRegion; + } else { + delete process.env.AWS_DEFAULT_REGION; + } }); describe('retrieveSubjectToken()', () => { @@ -614,10 +622,33 @@ describe('AwsClient', () => { scope.done(); }); - it('should resolve when AWS region is set as environment variable', async () => { + it('should resolve when AWS_REGION is set as environment variable', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + process.env.AWS_REGION = awsRegion; + + const client = new AwsClient(awsOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + }); + + it('should resolve when AWS_DEFAULT_REGION is set as environment variable', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + process.env.AWS_DEFAULT_REGION = awsRegion; + + const client = new AwsClient(awsOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + }); + + it('should prioritize AWS_REGION over AWS_DEFAULT_REGION environment variable', async () => { process.env.AWS_ACCESS_KEY_ID = accessKeyId; process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; process.env.AWS_REGION = awsRegion; + process.env.AWS_DEFAULT_REGION = 'fail-if-used'; const client = new AwsClient(awsOptions); const subjectToken = await client.retrieveSubjectToken(); From 6d2fe39d96e5ca607eb4b195aa77936a808c8454 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 24 Mar 2021 09:55:38 -0700 Subject: [PATCH 232/662] chore: release 7.0.3 (#1151) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5612b2f4..8cf93093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.0.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.2...v7.0.3) (2021-03-23) + + +### Bug Fixes + +* support AWS_DEFAULT_REGION for determining AWS region ([#1149](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1149)) ([9ae2d30](https://www.github.com/googleapis/google-auth-library-nodejs/commit/9ae2d30c15c9bce3cae70ccbe6e227c096005695)) + ### [7.0.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.1...v7.0.2) (2021-02-10) diff --git a/package.json b/package.json index 498cadec..faf0a2d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.0.2", + "version": "7.0.3", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index a430f21e..1e6bc88c 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^7.0.2", + "google-auth-library": "^7.0.3", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From ec49fe6944d2dd603ae5a7f6fcb30eea56d7b36f Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Mon, 5 Apr 2021 10:57:21 -0700 Subject: [PATCH 233/662] build: revert mocha fix (#1148) --- system-test/test.kitchen.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index dfd0a424..630f9b82 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-undef */ // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); From 6c1c91dac6c31d762b03774a385d780a824fce97 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Mon, 5 Apr 2021 17:16:02 -0700 Subject: [PATCH 234/662] fix: do not suppress external project ID determination errors (#1153) 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. --- src/auth/googleauth.ts | 17 ++++++++++++----- test/test.googleauth.ts | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index cd6f6040..e6f81650 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -671,11 +671,18 @@ export class GoogleAuth { return null; } const creds = await this.getClient(); - try { - return await (creds as BaseExternalAccountClient).getProjectId(); - } catch (e) { - return null; - } + // 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 as BaseExternalAccountClient).getProjectId(); } /** diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index a3c78eb6..131daafe 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1968,7 +1968,7 @@ describe('googleauth', () => { await assert.rejects( auth.getProjectId(), - /Unable to detect a Project Id in the current environment/ + /The caller does not have permission/ ); scopes.forEach(s => s.done()); }); @@ -1980,7 +1980,7 @@ describe('googleauth', () => { await assert.rejects( auth.getProjectId(), - /Unable to detect a Project Id in the current environment/ + /The file at invalid does not exist, or it is not a file/ ); }); From 45a79cfc504c14e852999f40b16fb08ad2c4e38c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 09:23:47 -0700 Subject: [PATCH 235/662] chore: release 7.0.4 (#1156) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf93093..9a87c613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.0.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.3...v7.0.4) (2021-04-06) + + +### Bug Fixes + +* do not suppress external project ID determination errors ([#1153](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1153)) ([6c1c91d](https://www.github.com/googleapis/google-auth-library-nodejs/commit/6c1c91dac6c31d762b03774a385d780a824fce97)) + ### [7.0.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.2...v7.0.3) (2021-03-23) diff --git a/package.json b/package.json index faf0a2d6..65cd58ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.0.3", + "version": "7.0.4", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 1e6bc88c..016f73d7 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^7.0.3", + "google-auth-library": "^7.0.4", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 43a68bccbef3a9a128704ef78cf425853d2903e9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 14 Apr 2021 23:12:06 +0200 Subject: [PATCH 236/662] chore(deps): update dependency @types/sinon to v10 (#1157) [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [@types/sinon](https://togithub.com/DefinitelyTyped/DefinitelyTyped) | [`^9.0.0` -> `^10.0.0`](https://renovatebot.com/diffs/npm/@types%2fsinon/9.0.11/10.0.0) | [![age](https://badges.renovateapi.com/packages/npm/@types%2fsinon/10.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/@types%2fsinon/10.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/@types%2fsinon/10.0.0/compatibility-slim/9.0.11)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/@types%2fsinon/10.0.0/confidence-slim/9.0.11)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration :date: **Schedule**: "after 9am and before 3pm" (UTC). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65cd58ca..d9905281 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", "@types/node": "^10.5.1", - "@types/sinon": "^9.0.0", + "@types/sinon": "^10.0.0", "@types/tmp": "^0.2.0", "assert-rejects": "^1.0.0", "c8": "^7.0.0", From 2ba18e74a9e10611f39de083fa37c69e8fb338f5 Mon Sep 17 00:00:00 2001 From: Cassi Lup Date: Tue, 20 Apr 2021 03:08:11 +0300 Subject: [PATCH 237/662] docs: adding missing blank space (#1161) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 375f7bbc..4dfb178d 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ This library provides a variety of ways to authenticate to your Google services. - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). ## Application Default Credentials -This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. +This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. From 4f9ad7871bd498f74989649af56a25e0ce7d13ac Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 19 Apr 2021 17:16:04 -0700 Subject: [PATCH 238/662] chore: regenerate common templates (#1159) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/8d112339-8f12-4550-bf32-d8ee0f806c58/targets - [ ] To automatically regenerate this PR, check this box. (May take up to 24 hours.) Source-Link: https://github.com/googleapis/synthtool/commit/c6706ee5d693e9ae5967614170732646590d8374 Source-Link: https://github.com/googleapis/synthtool/commit/b33b0e2056a85fc2264b294f2cf47dcd45e95186 Source-Link: https://github.com/googleapis/synthtool/commit/898b38a6f4fab89a76dfb152480bb034a781331b --- .github/workflows/ci.yaml | 2 +- .kokoro/release/publish.cfg | 40 ------------------------------------- synth.metadata | 4 ++-- 3 files changed, 3 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 891c9253..3df11de4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,7 @@ on: push: branches: - - master + - $default-branch pull_request: name: ci jobs: diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index 15dee6a1..3ca302f2 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -1,23 +1,3 @@ -# Get npm token from Keystore -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "google_cloud_npm_token" - backend_type: FASTCONFIGPUSH - } - } -} - -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "yoshi-automation-github-key" - } - } -} - before_action { fetch_keystore { keystore_resource { @@ -27,26 +7,6 @@ before_action { } } -# Fetch magictoken to use with Magic Github Proxy -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "releasetool-magictoken" - } - } -} - -# Fetch api key to use with Magic Github Proxy -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "magic-github-proxy-api-key" - } - } -} - env_vars: { key: "SECRET_MANAGER_KEYS" value: "npm_publish_token,releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" diff --git a/synth.metadata b/synth.metadata index 04d7caca..97488908 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "c27dec9e07d4e6873fdc486d25ed7b50e9560ea6" + "sha": "43a68bccbef3a9a128704ef78cf425853d2903e9" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "57c23fa5705499a4181095ced81f0ee0933b64f6" + "sha": "c6706ee5d693e9ae5967614170732646590d8374" } } ] From b134da65f87be7f929ff50cbdfe849bd79a68639 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 20 Apr 2021 15:15:58 -0700 Subject: [PATCH 239/662] docs: add ADC instructions (#1163) --- README.md | 2 +- synth.metadata | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4dfb178d..375f7bbc 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ This library provides a variety of ways to authenticate to your Google services. - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). ## Application Default Credentials -This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. +This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. diff --git a/synth.metadata b/synth.metadata index 97488908..bc21dc98 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "43a68bccbef3a9a128704ef78cf425853d2903e9" + "sha": "4f9ad7871bd498f74989649af56a25e0ce7d13ac" } }, { From 1c07cecaeda6d614a0e3324fdbbb83d922b081e3 Mon Sep 17 00:00:00 2001 From: "google-cloud-policy-bot[bot]" <80869356+google-cloud-policy-bot[bot]@users.noreply.github.com> Date: Thu, 29 Apr 2021 00:14:04 +0000 Subject: [PATCH 240/662] chore: add SECURITY.md (#1167) add a security policy --- SECURITY.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..8b58ae9c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). + +The Google Security Team will respond within 5 working days of your report on g.co/vulnz. + +We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. From 241063a8c7d583df53ae616347edc532aec02165 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 28 Apr 2021 17:32:23 -0700 Subject: [PATCH 241/662] build: add generated-files bot config (#1169) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/c7c39697-fbe1-4f7b-bb2f-e61c0eeefef8/targets - [ ] To automatically regenerate this PR, check this box. (May take up to 24 hours.) Source-Link: https://github.com/googleapis/synthtool/commit/e6f3d54be015a394b6ab5a25903ec09062a2b424 Source-Link: https://github.com/googleapis/synthtool/commit/04573fd73f56791c659832aa84d35a4ec860d6f7 --- .github/generated-files-bot.yml | 13 +++++++++++++ .github/workflows/ci.yaml | 2 +- synth.metadata | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 .github/generated-files-bot.yml diff --git a/.github/generated-files-bot.yml b/.github/generated-files-bot.yml new file mode 100644 index 00000000..1b3ef1c7 --- /dev/null +++ b/.github/generated-files-bot.yml @@ -0,0 +1,13 @@ +generatedFiles: +- path: '.kokoro/**' + message: '`.kokoro` files are templated and should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' +- path: '.github/CODEOWNERS' + message: 'CODEOWNERS should instead be modified via the `codeowner_team` property in .repo-metadata.json' +- path: '.github/workflows/**' + message: '`.github/workflows` (GitHub Actions) should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' +- path: '.github/generated-files-bot.+(yml|yaml)' + message: '`.github/generated-files-bot.(yml|yaml)` should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' +- path: 'README.md' + message: '`README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/master/.readme-partials.yaml' +- path: 'samples/README.md' + message: '`samples/README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/master/.readme-partials.yaml' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3df11de4..891c9253 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,7 @@ on: push: branches: - - $default-branch + - master pull_request: name: ci jobs: diff --git a/synth.metadata b/synth.metadata index bc21dc98..34e85142 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "4f9ad7871bd498f74989649af56a25e0ce7d13ac" + "sha": "b134da65f87be7f929ff50cbdfe849bd79a68639" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "c6706ee5d693e9ae5967614170732646590d8374" + "sha": "e6f3d54be015a394b6ab5a25903ec09062a2b424" } } ] From 7870f64c900aa5323d21a60de3d22616717ff215 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 18 May 2021 14:57:23 -0700 Subject: [PATCH 242/662] build: remove codecov action (#1172) * build: remove codecov action Source-Author: Benjamin E. Coe Source-Date: Tue May 11 17:35:28 2021 -0700 Source-Repo: googleapis/synthtool Source-Sha: b891fb474173f810051a7fdb0d66915e0a9bc82f Source-Link: https://github.com/googleapis/synthtool/commit/b891fb474173f810051a7fdb0d66915e0a9bc82f * lint fix Co-authored-by: Takashi Matsuo --- .github/workflows/ci.yaml | 10 ----- browser-test/fixtures/keys.ts | 24 ++++-------- browser-test/test.oauth2.ts | 6 +-- src/auth/authclient.ts | 3 +- src/auth/baseexternalclient.ts | 5 +-- src/auth/googleauth.ts | 9 ++--- src/auth/jwtclient.ts | 8 ++-- synth.metadata | 4 +- system-test/test.kitchen.ts | 2 +- test/test.googleauth.ts | 68 +++++++++++++++++++++------------- test/test.oauth2common.ts | 24 +++++++++--- 11 files changed, 86 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 891c9253..dbcdc7ce 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,11 +24,6 @@ jobs: - run: rm -rf node_modules - run: npm install - run: npm test - - name: coverage - uses: codecov/codecov-action@v1 - with: - name: actions ${{ matrix.node }} - fail_ci_if_error: false windows: runs-on: windows-latest steps: @@ -38,11 +33,6 @@ jobs: node-version: 14 - run: npm install - run: npm test - - name: coverage - uses: codecov/codecov-action@v1 - with: - name: actions windows - fail_ci_if_error: false lint: runs-on: ubuntu-latest steps: diff --git a/browser-test/fixtures/keys.ts b/browser-test/fixtures/keys.ts index cdd5f770..d261edb3 100644 --- a/browser-test/fixtures/keys.ts +++ b/browser-test/fixtures/keys.ts @@ -16,29 +16,21 @@ // https://tools.ietf.org/html/rfc7517 export const privateKey = { kty: 'RSA', - n: - '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', + n: '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', e: 'AQAB', - d: - 'X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q', - p: - '83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs', - q: - '3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk', - dp: - 'G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0', - dq: - 's9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk', - qi: - 'GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU', + d: 'X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q', + p: '83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs', + q: '3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk', + dp: 'G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0', + dq: 's9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk', + qi: 'GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU', alg: 'RS256', kid: '2011-04-29', }; export const publicKey = { kty: 'RSA', - n: - '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', + n: '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', e: 'AQAB', alg: 'RS256', kid: '2011-04-29', diff --git a/browser-test/test.oauth2.ts b/browser-test/test.oauth2.ts index d94bbabd..ffcb79c9 100644 --- a/browser-test/test.oauth2.ts +++ b/browser-test/test.oauth2.ts @@ -38,8 +38,7 @@ const FEDERATED_SIGNON_JWK_CERTS = [ e: 'AQAB', kty: 'RSA', alg: 'RS256', - n: - 'o27xh_y7EEIoBXJuXzgfvFY80Cbk8Efn2b5ZFEwPIwFFBoNxvfbRt3wsZoMulMcZbU5uQ8q82FZBUmpwAlybQ0pBm79XDnL0kEDl1pJjyuaNE4JGOdBosvG5_SBaa7CCq9ukTeTLZgDR_YfcmP4-XQfhWuS-vx7hTz13GzmVgO8FyMH4EYm2ZyOY-otx35sM6toF__W1MiGcwty4Dp0qPHeZ3a34saNc_miQS5lzMcUgMYBKCQZ-P7pSeQhgVmwGYWr_93fqZEPQdOFC-Qwgrww1dZ7cv9INkjFkBKiWQLEiXJKoUSp2BwL2CqENYhuS04g5ZkDV7lVpOxOuHucEzQ', + n: 'o27xh_y7EEIoBXJuXzgfvFY80Cbk8Efn2b5ZFEwPIwFFBoNxvfbRt3wsZoMulMcZbU5uQ8q82FZBUmpwAlybQ0pBm79XDnL0kEDl1pJjyuaNE4JGOdBosvG5_SBaa7CCq9ukTeTLZgDR_YfcmP4-XQfhWuS-vx7hTz13GzmVgO8FyMH4EYm2ZyOY-otx35sM6toF__W1MiGcwty4Dp0qPHeZ3a34saNc_miQS5lzMcUgMYBKCQZ-P7pSeQhgVmwGYWr_93fqZEPQdOFC-Qwgrww1dZ7cv9INkjFkBKiWQLEiXJKoUSp2BwL2CqENYhuS04g5ZkDV7lVpOxOuHucEzQ', use: 'sig', }, { @@ -48,8 +47,7 @@ const FEDERATED_SIGNON_JWK_CERTS = [ e: 'AQAB', kty: 'RSA', alg: 'RS256', - n: - 'sFlU5LpHUtYIm7B27iiu7c4ZPZk7ULUNmFdMVsTmYJxJqQBKUIKU9ozwF6TlUsECmYUMLpQhX_iHuaZRcpG2YiG7jbmi9HMlonIXX7uUe7PIf8rNHhveX_VI7ZpwPTnab3_7ciy_o8ZFde6KNltkx_DLRO6hXf6z6ow1APFIIriaNlF8niz5cy0fPIv0e_Z2p13Sz3mnAACjBKZGPw2X9GWh5XpRoDEQBcibXpeLuA7ti8zLZuH-9ybXOoou699fr4QHFxUkcd_8fFqmzO5PKnlOnJZ0gtuXCCYYc9XPX-WSqlqbGNMZy0Giu2wHbNbeWdepkgVlGuJonTnMx4gLuQ', + n: 'sFlU5LpHUtYIm7B27iiu7c4ZPZk7ULUNmFdMVsTmYJxJqQBKUIKU9ozwF6TlUsECmYUMLpQhX_iHuaZRcpG2YiG7jbmi9HMlonIXX7uUe7PIf8rNHhveX_VI7ZpwPTnab3_7ciy_o8ZFde6KNltkx_DLRO6hXf6z6ow1APFIIriaNlF8niz5cy0fPIv0e_Z2p13Sz3mnAACjBKZGPw2X9GWh5XpRoDEQBcibXpeLuA7ti8zLZuH-9ybXOoou699fr4QHFxUkcd_8fFqmzO5PKnlOnJZ0gtuXCCYYc9XPX-WSqlqbGNMZy0Giu2wHbNbeWdepkgVlGuJonTnMx4gLuQ', }, ]; const FEDERATED_SIGNON_JWK_CERTS_AXIOS_RESPONSE = { diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index bc1dea8b..49a0c01f 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -86,7 +86,8 @@ export declare interface AuthClient { export abstract class AuthClient extends EventEmitter - implements CredentialsClient { + implements CredentialsClient +{ protected quotaProjectId?: string; transporter = new DefaultTransporter(); credentials: Credentials = {}; diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 2d0aa10c..ea33f4e9 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -445,9 +445,8 @@ export abstract class BaseExternalAccountClient extends AuthClient { }, responseType: 'json', }; - const response = await this.transporter.request( - opts - ); + const response = + await this.transporter.request(opts); const successResponse = response.data; return { access_token: successResponse.accessToken, diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index e6f81650..6b84e6f6 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -261,9 +261,8 @@ export class GoogleAuth { // 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 - ); + credential = + await this._tryGetApplicationCredentialsFromEnvironmentVariable(options); if (credential) { if (credential instanceof JWT) { credential.defaultScopes = this.defaultScopes; @@ -609,8 +608,8 @@ export class GoogleAuth { exec('gcloud config config-helper --format json', (err, stdout) => { if (!err && stdout) { try { - const projectId = JSON.parse(stdout).configuration.properties.core - .project; + const projectId = + JSON.parse(stdout).configuration.properties.core.project; resolve(projectId); return; } catch (e) { diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 344aff62..38d94a11 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -124,9 +124,11 @@ export class JWT extends OAuth2Client implements IdTokenProvider { if (!this.apiKey && !this.hasUserScopes() && url) { if ( this.additionalClaims && - (this.additionalClaims as { - target_audience: string; - }).target_audience + ( + this.additionalClaims as { + target_audience: string; + } + ).target_audience ) { const {tokens} = await this.refreshToken(); return { diff --git a/synth.metadata b/synth.metadata index 34e85142..6907f3f1 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "b134da65f87be7f929ff50cbdfe849bd79a68639" + "sha": "241063a8c7d583df53ae616347edc532aec02165" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "e6f3d54be015a394b6ab5a25903ec09062a2b424" + "sha": "b891fb474173f810051a7fdb0d66915e0a9bc82f" } } ] diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 630f9b82..9a298958 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -22,7 +22,7 @@ import * as path from 'path'; import * as tmp from 'tmp'; import {promisify} from 'util'; -const mvp = (promisify(mv) as {}) as (...args: string[]) => Promise; +const mvp = promisify(mv) as {} as (...args: string[]) => Promise; const ncpp = promisify(ncp); const keep = !!process.env.GALN_KEEP_TEMPDIRS; const stagingDir = tmp.dirSync({keep, unsafeCleanup: true}); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 131daafe..44b93194 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -629,14 +629,16 @@ describe('googleauth', () => { it('tryGetApplicationCredentialsFromEnvironmentVariable should return null when env const is not set', async () => { // Set up a mock to return a null path string. mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS'); - const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + const client = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); assert.strictEqual(client, null); }); it('tryGetApplicationCredentialsFromEnvironmentVariable should return null when env const is empty string', async () => { // Set up a mock to return an empty path string. mockEnvVar('GOOGLE_APPLICATION_CREDENTIALS'); - const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + const client = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); assert.strictEqual(client, null); }); @@ -657,7 +659,8 @@ describe('googleauth', () => { 'GOOGLE_APPLICATION_CREDENTIALS', './test/fixtures/private.json' ); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + const result = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); const jwt = result as JWT; assert.strictEqual(privateJSON.private_key, jwt.key); assert.strictEqual(privateJSON.client_email, jwt.email); @@ -672,9 +675,10 @@ describe('googleauth', () => { 'GOOGLE_APPLICATION_CREDENTIALS', './test/fixtures/private.json' ); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable( - {eagerRefreshThresholdMillis: 60 * 60 * 1000} - ); + const result = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable({ + eagerRefreshThresholdMillis: 60 * 60 * 1000, + }); const jwt = result as JWT; assert.strictEqual(privateJSON.private_key, jwt.key); assert.strictEqual(privateJSON.client_email, jwt.email); @@ -687,14 +691,16 @@ describe('googleauth', () => { it('_tryGetApplicationCredentialsFromWellKnownFile should build the correct directory for Windows', async () => { mockWindows(); mockWindowsWellKnownFile(); - const result = (await auth._tryGetApplicationCredentialsFromWellKnownFile()) as JWT; + const result = + (await auth._tryGetApplicationCredentialsFromWellKnownFile()) as JWT; assert.ok(result); assert.strictEqual(result.email, private2JSON.client_email); }); it('_tryGetApplicationCredentialsFromWellKnownFile should build the correct directory for non-Windows', async () => { mockLinuxWellKnownFile(); - const client = (await auth._tryGetApplicationCredentialsFromWellKnownFile()) as JWT; + const client = + (await auth._tryGetApplicationCredentialsFromWellKnownFile()) as JWT; assert.strictEqual(client.email, private2JSON.client_email); }); @@ -702,25 +708,29 @@ describe('googleauth', () => { mockWindows(); mockEnvVar('APPDATA'); mockWindowsWellKnownFile(); - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); assert.strictEqual(null, result); }); it('_tryGetApplicationCredentialsFromWellKnownFile should fail on non-Windows when HOME is not defined', async () => { mockEnvVar('HOME'); mockLinuxWellKnownFile(); - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); assert.strictEqual(null, result); }); it('_tryGetApplicationCredentialsFromWellKnownFile should fail on Windows when file does not exist', async () => { mockWindows(); - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); assert.strictEqual(null, result); }); it('_tryGetApplicationCredentialsFromWellKnownFile should fail on non-Windows when file does not exist', async () => { - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); assert.strictEqual(null, result); }); @@ -846,7 +856,7 @@ describe('googleauth', () => { configuration: {properties: {core: {project: STUB_PROJECT}}}, }); - ((child_process.exec as unknown) as sinon.SinonStub).restore(); + (child_process.exec as unknown as sinon.SinonStub).restore(); const stub = sandbox .stub(child_process, 'exec') .callsArgWith(1, null, stdout, null); @@ -858,7 +868,7 @@ describe('googleauth', () => { it('getProjectId should use GCE when well-known file and env const are not set', async () => { const scope = createGetProjectIdNock(STUB_PROJECT); const projectId = await auth.getProjectId(); - const stub = (child_process.exec as unknown) as sinon.SinonStub; + const stub = child_process.exec as unknown as sinon.SinonStub; stub.restore(); assert(stub.calledOnce); assert.strictEqual(projectId, STUB_PROJECT); @@ -1150,7 +1160,8 @@ describe('googleauth', () => { 'GOOGLE_APPLICATION_CREDENTIALS', './test/fixtures/private.json' ); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + const result = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); assert(result); const jwt = result as JWT; const body = await auth.getCredentials(); @@ -1169,7 +1180,8 @@ describe('googleauth', () => { const spy = sinon.spy(auth, 'getClient'); const body = await auth.getCredentials(); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + const result = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); if (!(result instanceof JWT)) { throw new assert.AssertionError({ message: 'Credentials are not a JWT object', @@ -1198,7 +1210,8 @@ describe('googleauth', () => { it('getCredentials should return error when env const is not set', async () => { // Set up a mock to return a null path string - const client = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + const client = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); assert.strictEqual(null, client); await assert.rejects(auth.getCredentials()); }); @@ -1900,9 +1913,10 @@ describe('googleauth', () => { describe('getApplicationCredentialsFromFilePath()', () => { it('should correctly read the file and create a valid client', async () => { - const actualClient = await auth._getApplicationCredentialsFromFilePath( - './test/fixtures/external-account-cred.json' - ); + const actualClient = + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/external-account-cred.json' + ); assertExternalAccountClientInitialized( actualClient, @@ -2002,9 +2016,10 @@ describe('googleauth', () => { 'GOOGLE_APPLICATION_CREDENTIALS', './test/fixtures/external-account-cred.json' ); - const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable( - refreshOptions - ); + const result = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable( + refreshOptions + ); assert(result); assertExternalAccountClientInitialized( @@ -2017,9 +2032,10 @@ describe('googleauth', () => { it('tryGetApplicationCredentialsFromWellKnownFile() should resolve', async () => { // Set up a mock to return path to a valid credentials file. mockLinuxWellKnownFile('./test/fixtures/external-account-cred.json'); - const result = await auth._tryGetApplicationCredentialsFromWellKnownFile( - refreshOptions - ); + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile( + refreshOptions + ); assert(result); assertExternalAccountClientInitialized( diff --git a/test/test.oauth2common.ts b/test/test.oauth2common.ts index 5f57649b..9b0a485f 100644 --- a/test/test.oauth2common.ts +++ b/test/test.oauth2common.ts @@ -104,7 +104,9 @@ describe('OAuthClientAuthHandler', () => { }; const actualOptions = Object.assign({}, originalOptions); const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = `Basic ${expectedBase64EncodedCred}`; + ( + expectedOptions.headers as Headers + ).Authorization = `Basic ${expectedBase64EncodedCred}`; handler.testApplyClientAuthenticationOptions(actualOptions); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -125,7 +127,9 @@ describe('OAuthClientAuthHandler', () => { }; const actualOptions = Object.assign({}, originalOptions); const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = `Basic ${expectedBase64EncodedCredNoSecret}`; + ( + expectedOptions.headers as Headers + ).Authorization = `Basic ${expectedBase64EncodedCredNoSecret}`; handler.testApplyClientAuthenticationOptions(actualOptions); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -142,7 +146,9 @@ describe('OAuthClientAuthHandler', () => { }; const actualOptions = Object.assign({}, originalOptions); const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = `Basic ${expectedBase64EncodedCred}`; + ( + expectedOptions.headers as Headers + ).Authorization = `Basic ${expectedBase64EncodedCred}`; handler.testApplyClientAuthenticationOptions(actualOptions); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -343,7 +349,9 @@ describe('OAuthClientAuthHandler', () => { }; const actualOptions = Object.assign({}, originalOptions); const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = `Bearer ${bearerToken}`; + ( + expectedOptions.headers as Headers + ).Authorization = `Bearer ${bearerToken}`; handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -366,7 +374,9 @@ describe('OAuthClientAuthHandler', () => { const actualOptions = Object.assign({}, originalOptions); // Expected options should have bearer token in header. const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = `Bearer ${bearerToken}`; + ( + expectedOptions.headers as Headers + ).Authorization = `Bearer ${bearerToken}`; handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -389,7 +399,9 @@ describe('OAuthClientAuthHandler', () => { const actualOptions = Object.assign({}, originalOptions); // Expected options should have bearer token in header. const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = `Bearer ${bearerToken}`; + ( + expectedOptions.headers as Headers + ).Authorization = `Bearer ${bearerToken}`; handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); assert.deepStrictEqual(expectedOptions, actualOptions); From f377adc01ca16687bb905aa14f6c62a23e90aaa9 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 19 May 2021 09:22:14 -0700 Subject: [PATCH 243/662] feat: add `gcf-owl-bot[bot]` to `ignoreAuthors` (#1174) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/3fd0ea60-3653-4654-88eb-9829aab9995d/targets - [ ] To automatically regenerate this PR, check this box. (May take up to 24 hours.) Source-Link: https://github.com/googleapis/synthtool/commit/7332178a11ddddc91188dc0f25bca1ccadcaa6c6 --- .github/generated-files-bot.yml | 3 +++ synth.metadata | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/generated-files-bot.yml b/.github/generated-files-bot.yml index 1b3ef1c7..6b04910c 100644 --- a/.github/generated-files-bot.yml +++ b/.github/generated-files-bot.yml @@ -11,3 +11,6 @@ generatedFiles: message: '`README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/master/.readme-partials.yaml' - path: 'samples/README.md' message: '`samples/README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/master/.readme-partials.yaml' +ignoreAuthors: +- 'gcf-owl-bot[bot]' +- 'yoshi-automation' diff --git a/synth.metadata b/synth.metadata index 6907f3f1..ad67a1be 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "241063a8c7d583df53ae616347edc532aec02165" + "sha": "7870f64c900aa5323d21a60de3d22616717ff215" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "b891fb474173f810051a7fdb0d66915e0a9bc82f" + "sha": "7332178a11ddddc91188dc0f25bca1ccadcaa6c6" } } ] From 1f4497044fe3e1cd82e8d57cc5183724a5dffe6e Mon Sep 17 00:00:00 2001 From: Jeffrey Rennie Date: Thu, 20 May 2021 17:22:53 -0700 Subject: [PATCH 244/662] chore: migrate to owl bot (#1178) * chore: migrate to owl bot * chore: copy files from googleapis-gen 397c0bfd367a2427104f988d5329bc117caafd95 * chore: run the post processor --- .github/.OwlBot.lock.yaml | 4 ++++ .github/.OwlBot.yaml | 19 +++++++++++++++++++ synth.metadata | 18 ------------------ synth.py | 11 ----------- 4 files changed, 23 insertions(+), 29 deletions(-) create mode 100644 .github/.OwlBot.lock.yaml create mode 100644 .github/.OwlBot.yaml delete mode 100644 synth.metadata delete mode 100644 synth.py diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml new file mode 100644 index 00000000..6e5f21ef --- /dev/null +++ b/.github/.OwlBot.lock.yaml @@ -0,0 +1,4 @@ +docker: + digest: sha256:f556e6e7be625deb1b2429fe608df27be57185c3e6b7d39ee0059f1609f17530 + image: gcr.io/repo-automation-bots/owlbot-nodejs:latest + diff --git a/.github/.OwlBot.yaml b/.github/.OwlBot.yaml new file mode 100644 index 00000000..3a281cc9 --- /dev/null +++ b/.github/.OwlBot.yaml @@ -0,0 +1,19 @@ +# Copyright 2021 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. +docker: + image: gcr.io/repo-automation-bots/owlbot-nodejs:latest + + +begin-after-commit-hash: 397c0bfd367a2427104f988d5329bc117caafd95 + diff --git a/synth.metadata b/synth.metadata deleted file mode 100644 index ad67a1be..00000000 --- a/synth.metadata +++ /dev/null @@ -1,18 +0,0 @@ -{ - "sources": [ - { - "git": { - "name": ".", - "remote": "https://github.com/googleapis/google-auth-library-nodejs.git", - "sha": "7870f64c900aa5323d21a60de3d22616717ff215" - } - }, - { - "git": { - "name": "synthtool", - "remote": "https://github.com/googleapis/synthtool.git", - "sha": "7332178a11ddddc91188dc0f25bca1ccadcaa6c6" - } - } - ] -} \ No newline at end of file diff --git a/synth.py b/synth.py deleted file mode 100644 index 294439b0..00000000 --- a/synth.py +++ /dev/null @@ -1,11 +0,0 @@ -import synthtool as s -import synthtool.gcp as gcp -import synthtool.languages.node as node -import logging -logging.basicConfig(level=logging.DEBUG) - -AUTOSYNTH_MULTIPLE_COMMITS = True - -common_templates = gcp.CommonTemplates() -templates = common_templates.node_library() -s.copy(templates) From 4512363cf712dff5f178af0e7022c258775dfec7 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 21 May 2021 09:40:17 -0700 Subject: [PATCH 245/662] feat: add detection for Cloud Run (#1177) --- src/auth/envDetect.ts | 12 ++++++++++++ test/test.googleauth.ts | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/auth/envDetect.ts b/src/auth/envDetect.ts index 2d78842f..506be799 100644 --- a/src/auth/envDetect.ts +++ b/src/auth/envDetect.ts @@ -19,6 +19,7 @@ export enum GCPEnv { KUBERNETES_ENGINE = 'KUBERNETES_ENGINE', CLOUD_FUNCTIONS = 'CLOUD_FUNCTIONS', COMPUTE_ENGINE = 'COMPUTE_ENGINE', + CLOUD_RUN = 'CLOUD_RUN', NONE = 'NONE', } @@ -45,6 +46,8 @@ async function getEnvMemoized(): Promise { } else if (await isComputeEngine()) { if (await isKubernetesEngine()) { env = GCPEnv.KUBERNETES_ENGINE; + } else if (isCloudRun()) { + env = GCPEnv.CLOUD_RUN; } else { env = GCPEnv.COMPUTE_ENGINE; } @@ -62,6 +65,15 @@ function isCloudFunction() { return !!(process.env.FUNCTION_NAME || process.env.FUNCTION_TARGET); } +/** + * This check only verifies that the environment is running knative. + * This must be run *after* checking for Kubernetes, otherwise it will + * return a false positive. + */ +function isCloudRun() { + return !!process.env.K_CONFIGURATION; +} + async function isKubernetesEngine() { try { await gcpMetadata.instance('attributes/cluster-name'); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 44b93194..0a11628e 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1367,6 +1367,14 @@ describe('googleauth', () => { assert.strictEqual(env, envDetect.GCPEnv.APP_ENGINE); }); + it('should get the current environment if Cloud Run', async () => { + envDetect.clear(); + mockEnvVar('K_CONFIGURATION', 'KITTY'); + const {auth} = mockGCE(); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.CLOUD_RUN); + }); + it('should make the request', async () => { const url = 'http://example.com'; const {auth, scopes} = mockGCE(); From 93f1e3bf150e10617d1b13308263ea91c0a58f7d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 16:20:14 -0400 Subject: [PATCH 246/662] chore: release 7.1.0 (#1176) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a87c613..6ca0f602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.1.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.4...v7.1.0) (2021-05-21) + + +### Features + +* add `gcf-owl-bot[bot]` to `ignoreAuthors` ([#1174](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1174)) ([f377adc](https://www.github.com/googleapis/google-auth-library-nodejs/commit/f377adc01ca16687bb905aa14f6c62a23e90aaa9)) +* add detection for Cloud Run ([#1177](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1177)) ([4512363](https://www.github.com/googleapis/google-auth-library-nodejs/commit/4512363cf712dff5f178af0e7022c258775dfec7)) + ### [7.0.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.3...v7.0.4) (2021-04-06) diff --git a/package.json b/package.json index d9905281..d51a1af7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.0.4", + "version": "7.1.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 016f73d7..db650bf3 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^7.0.4", + "google-auth-library": "^7.1.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 202ec19181d91e239f0bf7aba5205e26f88a12e5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 25 May 2021 17:54:32 +0200 Subject: [PATCH 247/662] chore(deps): update dependency sinon to v11 (#1181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [sinon](https://sinonjs.org/) ([source](https://togithub.com/sinonjs/sinon)) | [`^10.0.0` -> `^11.0.0`](https://renovatebot.com/diffs/npm/sinon/10.0.0/11.1.0) | [![age](https://badges.renovateapi.com/packages/npm/sinon/11.1.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/sinon/11.1.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/sinon/11.1.0/compatibility-slim/10.0.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/sinon/11.1.0/confidence-slim/10.0.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
sinonjs/sinon ### [`v11.1.0`](https://togithub.com/sinonjs/sinon/blob/master/CHANGELOG.md#​1110--2021-05-25) [Compare Source](https://togithub.com/sinonjs/sinon/compare/v11.0.0...31be9a5d5a4762ef01cb195f29024616dfee9ce8) \================== - Add sinon.promise() implementation ([#​2369](https://togithub.com/sinonjs/sinon/issues/2369)) - Set wrappedMethod on getters/setters ([#​2378](https://togithub.com/sinonjs/sinon/issues/2378)) - \[Docs] Update fake-server usage & descriptions ([#​2365](https://togithub.com/sinonjs/sinon/issues/2365)) - Fake docs improvement ([#​2360](https://togithub.com/sinonjs/sinon/issues/2360)) - Update nise to 5.1.0 (fixed [#​2318](https://togithub.com/sinonjs/sinon/issues/2318)) ### [`v11.0.0`](https://togithub.com/sinonjs/sinon/blob/master/CHANGELOG.md#​1100--2021-05-24) [Compare Source](https://togithub.com/sinonjs/sinon/compare/v10.0.1...v11.0.0) \================== - Explicitly use samsam 6.0.2 with fix for [#​2345](https://togithub.com/sinonjs/sinon/issues/2345) - Update most packages ([#​2371](https://togithub.com/sinonjs/sinon/issues/2371)) - Update compatibility docs ([#​2366](https://togithub.com/sinonjs/sinon/issues/2366)) - Update packages (includes breaking fake-timers change, see [#​2352](https://togithub.com/sinonjs/sinon/issues/2352)) - Warn of potential memory leaks ([#​2357](https://togithub.com/sinonjs/sinon/issues/2357)) - Fix clock test errors ### [`v10.0.1`](https://togithub.com/sinonjs/sinon/blob/master/CHANGELOG.md#​1001--2021-04-08) [Compare Source](https://togithub.com/sinonjs/sinon/compare/v10.0.0...v10.0.1) \================== - Upgrade sinon components (bumps y18n to 4.0.1) - Bump y18n from 4.0.0 to 4.0.1
--- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻️ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d51a1af7..d4b02c39 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "nock": "^13.0.0", "null-loader": "^4.0.0", "puppeteer": "^8.0.0", - "sinon": "^10.0.0", + "sinon": "^11.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "^3.8.3", From 003e3ee5d8aeb749c07a4a4db2b75a5882988cc3 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 31 May 2021 17:25:14 +0200 Subject: [PATCH 248/662] fix(deps): update dependency puppeteer to v10 (#1182) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d4b02c39..89d14f36 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^8.0.0", + "puppeteer": "^10.0.0", "sinon": "^11.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 95e71274..1c350860 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^7.0.0", - "puppeteer": "^8.0.0" + "puppeteer": "^10.0.0" } } From e9724d88c85b73eb61a78d46837068a6c734534b Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Tue, 1 Jun 2021 18:08:01 -0700 Subject: [PATCH 249/662] chore(deps): upgrade @types/node 14.x (#1184) --- package.json | 2 +- src/auth/oauth2client.ts | 14 +++++++++----- src/auth/stscredentials.ts | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 89d14f36..3b02c5ad 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@types/mocha": "^8.0.0", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", - "@types/node": "^10.5.1", + "@types/node": "^14.0.0", "@types/sinon": "^10.0.0", "@types/tmp": "^0.2.0", "assert-rejects": "^1.0.0", diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 20572d33..84420b6c 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -158,8 +158,8 @@ export interface TokenInfo { interface TokenInfoRequest { aud: string; user_id?: string; - scope: string; - expires_in: number; + scope?: string; + expires_in?: number; azp?: string; sub?: string; exp?: number; @@ -533,7 +533,11 @@ export class OAuth2Client extends AuthClient { opts.scope = opts.scope.join(' '); } const rootUrl = OAuth2Client.GOOGLE_OAUTH2_AUTH_BASE_URL_; - return rootUrl + '?' + querystring.stringify(opts); + return ( + rootUrl + + '?' + + querystring.stringify(opts as querystring.ParsedUrlQueryInput) + ); } generateCodeVerifier(): void { @@ -1024,8 +1028,8 @@ export class OAuth2Client extends AuthClient { }); const info = Object.assign( { - expiry_date: new Date().getTime() + data.expires_in * 1000, - scopes: data.scope.split(' '), + expiry_date: new Date().getTime() + data.expires_in! * 1000, + scopes: data.scope!.split(' '), }, data ); diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index c86e76cf..65b22d0c 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -198,7 +198,9 @@ export class StsCredentials extends OAuthClientAuthHandler { url: this.tokenExchangeEndpoint, method: 'POST', headers, - data: querystring.stringify(values), + data: querystring.stringify( + values as unknown as querystring.ParsedUrlQueryInput + ), responseType: 'json', }; // Apply OAuth client authentication. From 0638d0f724e4bc5adcf7bbb2617102b72a216ada Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 2 Jun 2021 03:16:37 +0200 Subject: [PATCH 250/662] chore(deps): update dependency @types/node to v14 (#1180) --- system-test/fixtures/kitchen/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index 49f2f1fd..5540332c 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -17,7 +17,7 @@ "google-auth-library": "file:./google-auth-library.tgz" }, "devDependencies": { - "@types/node": "^10.3.0", + "@types/node": "^14.0.0", "typescript": "^3.0.0", "gts": "^2.0.0", "null-loader": "^4.0.0", From f9c0e7b058c88cecfe1a3dba9b2f069819dfe1d4 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 2 Jun 2021 11:48:20 -0700 Subject: [PATCH 251/662] chore: release 7.1.1 (#1185) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Jeffrey Rennie --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca0f602..ef8210cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.1.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.0...v7.1.1) (2021-06-02) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v10 ([#1182](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1182)) ([003e3ee](https://www.github.com/googleapis/google-auth-library-nodejs/commit/003e3ee5d8aeb749c07a4a4db2b75a5882988cc3)) + ## [7.1.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.0.4...v7.1.0) (2021-05-21) diff --git a/package.json b/package.json index 3b02c5ad..d6895e7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.1.0", + "version": "7.1.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index db650bf3..e9e65a85 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^7.1.0", + "google-auth-library": "^7.1.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From cabeada5a6511b43eeb827a8a373ffa9f290d69d Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 19:02:44 +0000 Subject: [PATCH 252/662] chore: Report warning on `.github/workflows/ci.yaml` (#1187) * fix: Report warning on `.github/workflows/ci.yaml` Not all files in `.github/workflows` are managed, only `ci.yaml`. Related false-positive: https://github.com/googleapis/repo-automation-bots/pull/1952#issuecomment-856142886 * fix: Report warning on `.github/workflows/ci.yaml` Not all files in `.github/workflows` are managed, only `ci.yaml`. Source-Link: https://github.com/googleapis/synthtool/commit/2430f8d90ed8a508e8422a3a7191e656d5a6bf53 Post-Processor: gcr.io/repo-automation-bots/owlbot-nodejs:latest@sha256:14aaee566d6fc07716bb92da416195156e47a4777e7d1cd2bb3e28c46fe30fe2 --- .github/.OwlBot.lock.yaml | 5 ++--- .github/generated-files-bot.yml | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 6e5f21ef..3a93af92 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,4 +1,3 @@ docker: - digest: sha256:f556e6e7be625deb1b2429fe608df27be57185c3e6b7d39ee0059f1609f17530 - image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - + image: gcr.io/repo-automation-bots/owlbot-nodejs:latest + digest: sha256:14aaee566d6fc07716bb92da416195156e47a4777e7d1cd2bb3e28c46fe30fe2 diff --git a/.github/generated-files-bot.yml b/.github/generated-files-bot.yml index 6b04910c..7bb7ce54 100644 --- a/.github/generated-files-bot.yml +++ b/.github/generated-files-bot.yml @@ -3,8 +3,8 @@ generatedFiles: message: '`.kokoro` files are templated and should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' - path: '.github/CODEOWNERS' message: 'CODEOWNERS should instead be modified via the `codeowner_team` property in .repo-metadata.json' -- path: '.github/workflows/**' - message: '`.github/workflows` (GitHub Actions) should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' +- path: '.github/workflows/ci.yaml' + message: '`.github/workflows/ci.yaml` (GitHub Actions) should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' - path: '.github/generated-files-bot.+(yml|yaml)' message: '`.github/generated-files-bot.(yml|yaml)` should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' - path: 'README.md' From 74ac5db59f9eff8fa4f3bdb6acc0647a1a4f491f Mon Sep 17 00:00:00 2001 From: Xin Li Date: Wed, 9 Jun 2021 14:21:44 -0700 Subject: [PATCH 253/662] fix: use iam client library to setup test (#1173) --- samples/package.json | 1 + samples/scripts/externalclient-setup.js | 62 ++++++++++--------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/samples/package.json b/samples/package.json index e9e65a85..ca56b8e0 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,6 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@googleapis/iam": "^0.2.0", "google-auth-library": "^7.1.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", diff --git a/samples/scripts/externalclient-setup.js b/samples/scripts/externalclient-setup.js index e8a5ce4b..a3db1f65 100755 --- a/samples/scripts/externalclient-setup.js +++ b/samples/scripts/externalclient-setup.js @@ -84,6 +84,7 @@ const fs = require('fs'); const {promisify} = require('util'); const {GoogleAuth} = require('google-auth-library'); +const Iam = require('@googleapis/iam'); const readFile = promisify(fs.readFile); @@ -141,23 +142,21 @@ async function main(config) { const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform', }); - - // TODO: switch to using IAM client SDK once v1 API has all the v1beta - // changes. - // https://cloud.google.com/iam/docs/reference/rest/v1beta/projects.locations.workloadIdentityPools - // https://github.com/googleapis/google-api-nodejs-client/tree/master/src/apis/iam + const iam = await Iam.iam({ + version: 'v1', + auth, + }); // Create the workload identity pool. - response = await auth.request({ - url: - `https://iam.googleapis.com/v1beta/projects/${projectId}/` + - `locations/global/workloadIdentityPools?workloadIdentityPoolId=${poolId}`, - method: 'POST', - data: { + response = await iam.projects.locations.workloadIdentityPools.create({ + parent: `projects/${projectId}/locations/global`, + workloadIdentityPoolId: poolId, + requestBody: { displayName: 'Test workload identity pool', description: 'Test workload identity pool for Node.js', }, }); + // Populate the audience field. This will be used by the tests, specifically // the credential configuration file. const poolResourcePath = response.data.name.split('/operations')[0]; @@ -166,12 +165,10 @@ async function main(config) { // Allow service account impersonation. // Get the existing IAM policity bindings on the current service account. - response = await auth.request({ - url: - `https://iam.googleapis.com/v1/projects/${projectId}/` + - `serviceAccounts/${clientEmail}:getIamPolicy`, - method: 'POST', + response = await iam.projects.serviceAccounts.getIamPolicy({ + resource: `projects/${projectId}/serviceAccounts/${clientEmail}`, }); + const bindings = response.data.bindings || []; // If not found, add roles/iam.workloadIdentityUser role binding to the // workload identity pool member. @@ -203,12 +200,9 @@ async function main(config) { ], }); } - await auth.request({ - url: - `https://iam.googleapis.com/v1/projects/${projectId}/` + - `serviceAccounts/${clientEmail}:setIamPolicy`, - method: 'POST', - data: { + await iam.projects.serviceAccounts.setIamPolicy({ + resource: `projects/${projectId}/serviceAccounts/${clientEmail}`, + requestBody: { policy: { bindings, }, @@ -217,13 +211,10 @@ async function main(config) { // Create an OIDC provider. This will use the accounts.google.com issuer URL. // This will use the STS audience as the OIDC token audience. - await auth.request({ - url: - `https://iam.googleapis.com/v1beta/projects/${projectId}/` + - `locations/global/workloadIdentityPools/${poolId}/providers?` + - `workloadIdentityPoolProviderId=${oidcProviderId}`, - method: 'POST', - data: { + await iam.projects.locations.workloadIdentityPools.providers.create({ + parent: `projects/${projectId}/locations/global/workloadIdentityPools/${poolId}`, + workloadIdentityPoolProviderId: oidcProviderId, + requestBody: { displayName: 'Test OIDC provider', description: 'Test OIDC provider for Node.js', attributeMapping: { @@ -237,13 +228,10 @@ async function main(config) { }); // Create an AWS provider. - await auth.request({ - url: - `https://iam.googleapis.com/v1beta/projects/${projectId}/` + - `locations/global/workloadIdentityPools/${poolId}/providers?` + - `workloadIdentityPoolProviderId=${awsProviderId}`, - method: 'POST', - data: { + await iam.projects.locations.workloadIdentityPools.providers.create({ + parent: `projects/${projectId}/locations/global/workloadIdentityPools/${poolId}`, + workloadIdentityPoolProviderId: awsProviderId, + requestBody: { displayName: 'Test AWS provider', description: 'Test AWS provider for Node.js', aws: { @@ -292,7 +280,7 @@ main(config) console.log(`AUDIENCE_OIDC='${audiences.oidcAudience}'`); console.log(`AUDIENCE_AWS='${audiences.awsAudience}'`); console.log( - `AWS_ROLE_ARN='arn:aws::iam::${config.awsAccountId}:role/${config.awsRoleName}'` + `AWS_ROLE_ARN='arn:aws:iam::${config.awsAccountId}:role/${config.awsRoleName}'` ); }) .catch(console.error); From a29027be40d050a3af9816cb6a5fd2fb66802d25 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 10 Jun 2021 06:00:12 +0000 Subject: [PATCH 254/662] build: add auto-approve to Node libraries (#1100) (#1191) * build: add auto-approve to Node libraries Co-authored-by: Benjamin E. Coe Source-Link: https://github.com/googleapis/synthtool/commit/5cae043787729a908ed0cab28ca27baf9acee3c4 Post-Processor: gcr.io/repo-automation-bots/owlbot-nodejs:latest@sha256:65aa68f2242c172345d7c1e780bced839bfdc344955d6aa460aa63b4481d93e5 --- .github/.OwlBot.lock.yaml | 2 +- .github/CODEOWNERS | 3 ++ .github/auto-approve.yml | 7 +++++ .kokoro/release/docs-devsite.cfg | 2 +- .kokoro/release/docs-devsite.sh | 48 ++------------------------------ .trampolinerc | 3 +- 6 files changed, 17 insertions(+), 48 deletions(-) create mode 100644 .github/auto-approve.yml diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 3a93af92..1b520297 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - digest: sha256:14aaee566d6fc07716bb92da416195156e47a4777e7d1cd2bb3e28c46fe30fe2 + digest: sha256:65aa68f2242c172345d7c1e780bced839bfdc344955d6aa460aa63b4481d93e5 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d904d1e2..80520bba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,3 +7,6 @@ # The yoshi-nodejs team is the default owner for nodejs repositories. * @googleapis/yoshi-nodejs + +# The github automation team is the default owner for the auto-approve file. +.github/auto-approve.yml @googleapis/github-automation diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml new file mode 100644 index 00000000..90369797 --- /dev/null +++ b/.github/auto-approve.yml @@ -0,0 +1,7 @@ +rules: +- author: "release-please[bot]" + title: "^chore: release" + changedFiles: + - "package\\.json$" + - "CHANGELOG\\.md$" + maxFiles: 3 \ No newline at end of file diff --git a/.kokoro/release/docs-devsite.cfg b/.kokoro/release/docs-devsite.cfg index c37e25cd..3446a019 100644 --- a/.kokoro/release/docs-devsite.cfg +++ b/.kokoro/release/docs-devsite.cfg @@ -11,7 +11,7 @@ before_action { # doc publications use a Python image. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" } # Download trampoline resources. diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index 7657be33..2198e67f 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2019 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ set -eo pipefail -# build jsdocs (Python is installed on the Node 10 docker image). if [[ -z "$CREDENTIALS" ]]; then # if CREDENTIALS are explicitly set, assume we're testing locally # and don't set NPM_CONFIG_PREFIX. @@ -25,47 +24,6 @@ if [[ -z "$CREDENTIALS" ]]; then cd $(dirname $0)/../.. fi -mkdir ./etc - npm install -npm run api-extractor -npm run api-documenter - -npm i json@9.0.6 -g -NAME=$(cat .repo-metadata.json | json name) - -mkdir ./_devsite -cp ./yaml/$NAME/* ./_devsite - -# Clean up TOC -# Delete SharePoint item, see https://github.com/microsoft/rushstack/issues/1229 -sed -i -e '1,3d' ./yaml/toc.yml -sed -i -e 's/^ //' ./yaml/toc.yml -# Delete interfaces from TOC (name and uid) -sed -i -e '/name: I[A-Z]/{N;d;}' ./yaml/toc.yml -sed -i -e '/^ *\@google-cloud.*:interface/d' ./yaml/toc.yml - -cp ./yaml/toc.yml ./_devsite/toc.yml - -# create docs.metadata, based on package.json and .repo-metadata.json. -pip install -U pip -python3 -m pip install --user gcp-docuploader -python3 -m docuploader create-metadata \ - --name=$NAME \ - --version=$(cat package.json | json version) \ - --language=$(cat .repo-metadata.json | json language) \ - --distribution-name=$(cat .repo-metadata.json | json distribution_name) \ - --product-page=$(cat .repo-metadata.json | json product_documentation) \ - --github-repository=$(cat .repo-metadata.json | json repo) \ - --issue-tracker=$(cat .repo-metadata.json | json issue_tracker) -cp docs.metadata ./_devsite/docs.metadata - -# deploy the docs. -if [[ -z "$CREDENTIALS" ]]; then - CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account -fi -if [[ -z "$BUCKET" ]]; then - BUCKET=docs-staging-v2 -fi - -python3 -m docuploader upload ./_devsite --destination-prefix docfx --credentials $CREDENTIALS --staging-bucket $BUCKET +npm install --no-save @google-cloud/cloud-rad@^0.2.5 +npx @google-cloud/cloud-rad \ No newline at end of file diff --git a/.trampolinerc b/.trampolinerc index 164613b9..d4fcb894 100644 --- a/.trampolinerc +++ b/.trampolinerc @@ -20,7 +20,8 @@ required_envvars+=( # Add env vars which are passed down into the container here. pass_down_envvars+=( - "AUTORELEASE_PR" + "AUTORELEASE_PR", + "VERSION" ) # Prevent unintentional override on the default image. From ce09438dfbbc0f1bd0a5e862ef2809a4a9d6f69d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 10 Jun 2021 06:20:11 +0000 Subject: [PATCH 255/662] chore: release 7.1.2 (#1190) :robot: I have created a release \*beep\* \*boop\* --- ### [7.1.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.1...v7.1.2) (2021-06-10) ### Bug Fixes * use iam client library to setup test ([#1173](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1173)) ([74ac5db](https://www.github.com/googleapis/google-auth-library-nodejs/commit/74ac5db59f9eff8fa4f3bdb6acc0647a1a4f491f)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef8210cd..fbf5a726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.1.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.1...v7.1.2) (2021-06-10) + + +### Bug Fixes + +* use iam client library to setup test ([#1173](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1173)) ([74ac5db](https://www.github.com/googleapis/google-auth-library-nodejs/commit/74ac5db59f9eff8fa4f3bdb6acc0647a1a4f491f)) + ### [7.1.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.0...v7.1.1) (2021-06-02) diff --git a/package.json b/package.json index d6895e7b..64a9c5d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.1.1", + "version": "7.1.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index ca56b8e0..7512d147 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.1.1", + "google-auth-library": "^7.1.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From d3bb5267028131394a067501fe32807f4e8168e6 Mon Sep 17 00:00:00 2001 From: "F. Hinkelmann" Date: Thu, 10 Jun 2021 23:02:22 +0200 Subject: [PATCH 256/662] chore(nodejs): remove api-extractor dependencies (#1193) --- package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/package.json b/package.json index 64a9c5d5..c3472e59 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,6 @@ }, "devDependencies": { "@compodoc/compodoc": "^1.1.7", - "@microsoft/api-documenter": "^7.8.10", - "@microsoft/api-extractor": "^7.8.10", "@types/base64-js": "^1.2.5", "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", @@ -93,9 +91,7 @@ "docs-test": "linkinator docs", "predocs-test": "npm run docs", "prelint": "cd samples; npm link ../; npm install", - "precompile": "gts clean", - "api-extractor": "api-extractor run --local", - "api-documenter": "api-documenter yaml --input-folder=temp" + "precompile": "gts clean" }, "license": "Apache-2.0" } From f523f22c15a57e3acfecd7e0bd55dba4f676daf3 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 11 Jun 2021 19:10:29 +0000 Subject: [PATCH 257/662] build: remove errant comma (#1113) (#1194) Source-Link: https://github.com/googleapis/synthtool/commit/41ccd8cd13ec31f4fb839cf8182aea3c7156e19d Post-Processor: gcr.io/repo-automation-bots/owlbot-nodejs:latest@sha256:c9c7828c165b1985579098978877935ee52dda2b1b538087514fd24fa2443e7a --- .github/.OwlBot.lock.yaml | 2 +- .trampolinerc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 1b520297..e7c45fd3 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - digest: sha256:65aa68f2242c172345d7c1e780bced839bfdc344955d6aa460aa63b4481d93e5 + digest: sha256:c9c7828c165b1985579098978877935ee52dda2b1b538087514fd24fa2443e7a diff --git a/.trampolinerc b/.trampolinerc index d4fcb894..5fc22531 100644 --- a/.trampolinerc +++ b/.trampolinerc @@ -20,7 +20,7 @@ required_envvars+=( # Add env vars which are passed down into the container here. pass_down_envvars+=( - "AUTORELEASE_PR", + "AUTORELEASE_PR" "VERSION" ) From e82da6092d66902a1b9278aad401c36da2e12777 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 25 Jun 2021 19:18:32 +0000 Subject: [PATCH 258/662] build(node): do not throw on deprecation (#1140) (#1199) Refs https://github.com/googleapis/nodejs-service-usage/issues/22 Source-Link: https://github.com/googleapis/synthtool/commit/6d26b13debbfe3c6a6a9f9f1914c5bccf1e6fadc Post-Processor: gcr.io/repo-automation-bots/owlbot-nodejs:latest@sha256:e59b73e911585903ee6b8a1c5246e93d9e9463420f597b6eb2e4b616ee8a0fee --- .github/.OwlBot.lock.yaml | 2 +- .github/workflows/ci.yaml | 4 ++++ .kokoro/test.sh | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index e7c45fd3..26e91bb2 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - digest: sha256:c9c7828c165b1985579098978877935ee52dda2b1b538087514fd24fa2443e7a + digest: sha256:e59b73e911585903ee6b8a1c5246e93d9e9463420f597b6eb2e4b616ee8a0fee diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dbcdc7ce..f033c0d2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,6 +24,8 @@ jobs: - run: rm -rf node_modules - run: npm install - run: npm test + env: + MOCHA_THROW_DEPRECATION: false windows: runs-on: windows-latest steps: @@ -33,6 +35,8 @@ jobs: node-version: 14 - run: npm install - run: npm test + env: + MOCHA_THROW_DEPRECATION: false lint: runs-on: ubuntu-latest steps: diff --git a/.kokoro/test.sh b/.kokoro/test.sh index 5d6383fc..b5646aeb 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -32,6 +32,9 @@ if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ART } trap cleanup EXIT HUP fi +# Unit tests exercise the entire API surface, which may include +# deprecation warnings: +export MOCHA_THROW_DEPRECATION=false npm test # codecov combines coverage across integration and unit tests. Include From 75e74a96680443fe07a40f4ba1eaa07edf2fd9a7 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 30 Jun 2021 15:40:33 +0000 Subject: [PATCH 259/662] build: auto-approve renovate-bot PRs for minor updates (#1145) (#1202) Source-Link: https://github.com/googleapis/synthtool/commit/39652e3948f455fd0b77535a0145eeec561a3706 Post-Processor: gcr.io/repo-automation-bots/owlbot-nodejs:latest@sha256:41d5457ff79c3945782ab7e23bf4d617fd7bf3f2b03b6d84808010f7d2e10ca2 --- .github/.OwlBot.lock.yaml | 2 +- .github/auto-approve.yml | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 26e91bb2..9d507eee 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - digest: sha256:e59b73e911585903ee6b8a1c5246e93d9e9463420f597b6eb2e4b616ee8a0fee + digest: sha256:41d5457ff79c3945782ab7e23bf4d617fd7bf3f2b03b6d84808010f7d2e10ca2 diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml index 90369797..a79ba66c 100644 --- a/.github/auto-approve.yml +++ b/.github/auto-approve.yml @@ -4,4 +4,9 @@ rules: changedFiles: - "package\\.json$" - "CHANGELOG\\.md$" - maxFiles: 3 \ No newline at end of file + maxFiles: 3 +- author: "renovate-bot" + title: "^(fix\\(deps\\)|chore\\(deps\\)):" + changedFiles: + - "/package\\.json$" + maxFiles: 2 From faa6677fe72c8fc671a2190abe45897ac58cc42e Mon Sep 17 00:00:00 2001 From: Xin Li Date: Wed, 30 Jun 2021 12:41:06 -0700 Subject: [PATCH 260/662] feat: Implement DownscopedClient#getAccessToken() and unit test (#1201) * Use iam client library to setup test * fix: Update Node.js integration test setup to use iam client library. * fix: revert package.json changes * fix lint errors and improve code * direct use iam * update package version * Implement DownscopedClient#getAccessToken() and unit test Co-authored-by: Justin Beckwith --- src/auth/downscopedclient.ts | 254 +++++++++++++++++++++ src/index.ts | 1 + test/externalclienthelper.ts | 21 ++ test/test.downscopedclient.ts | 406 ++++++++++++++++++++++++++++++++++ test/test.index.ts | 1 + 5 files changed, 683 insertions(+) create mode 100644 src/auth/downscopedclient.ts create mode 100644 test/test.downscopedclient.ts diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts new file mode 100644 index 00000000..b7768cc8 --- /dev/null +++ b/src/auth/downscopedclient.ts @@ -0,0 +1,254 @@ +// Copyright 2021 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. + +import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; + +import {BodyResponseCallback} from '../transporters'; +import {Credentials} from './credentials'; +import {AuthClient} from './authclient'; + +import {GetAccessTokenResponse, Headers, RefreshOptions} from './oauth2client'; +import * as sts from './stscredentials'; + +/** + * The required token exchange grant_type: rfc8693#section-2.1 + */ +const STS_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'; +/** + * The requested token exchange requested_token_type: rfc8693#section-2.1 + */ +const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; +/** + * The requested token exchange subject_token_type: rfc8693#section-2.1 + */ +const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; +/** The STS access token exchange end point. */ +const STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1beta/token'; + +/** + * Offset to take into account network delays and server clock skews. + */ +export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000; + +/** + * Internal interface for tracking the access token expiration time. + */ +interface CredentialsWithResponse extends Credentials { + res?: GaxiosResponse | null; +} + +/** + * Defines an upper bound of permissions available for a GCP credential. + */ +interface CredentialAccessBoundary { + accessBoundary: { + accessBoundaryRules: AccessBoundaryRule[]; + }; +} + +/** Defines an upper bound of permissions on a particular resource. */ +interface AccessBoundaryRule { + availablePermissions: string[]; + availableResource: string; + availabilityCondition?: AvailabilityCondition; +} + +/** + * An optional condition that can be used as part of a + * CredentialAccessBoundary to further restrict permissions. + */ +interface AvailabilityCondition { + expression: string; + title?: string; + description?: string; +} + +export class DownscopedClient extends AuthClient { + /** + * OAuth scopes for the GCP access token to use. When not provided, + * the default https://www.googleapis.com/auth/cloud-platform is + * used. + */ + private cachedDownscopedAccessToken: CredentialsWithResponse | null; + private readonly stsCredential: sts.StsCredentials; + public readonly authClient: AuthClient; + public readonly credentialAccessBoundary: CredentialAccessBoundary; + public readonly eagerRefreshThresholdMillis: number; + public readonly forceRefreshOnFailure: boolean; + + constructor( + private client: AuthClient, + private cab: CredentialAccessBoundary, + additionalOptions?: RefreshOptions + ) { + super(); + + // Check a number of 1-10 access boundary rules are defined within credential access boundary. + if (cab.accessBoundary.accessBoundaryRules.length === 0) { + throw new Error('At least one access boundary rule needs to be defined.'); + } else if (cab.accessBoundary.accessBoundaryRules.length > 10) { + throw new Error('Access boundary rule exceeds limit, max 10 allowed.'); + } + + // Check at least one permission should be defined in each access boundary rule. + for (const rule of cab.accessBoundary.accessBoundaryRules) { + if (rule.availablePermissions.length === 0) { + throw new Error( + 'At least one permission should be defined in access boundary rules.' + ); + } + } + + this.stsCredential = new sts.StsCredentials(STS_ACCESS_TOKEN_URL); + // Default OAuth scope. This could be overridden via public property. + this.cachedDownscopedAccessToken = null; + this.credentialAccessBoundary = cab; + this.authClient = client; + // As threshold could be zero, + // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the + // zero value. + if (typeof additionalOptions?.eagerRefreshThresholdMillis !== 'number') { + this.eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET; + } else { + this.eagerRefreshThresholdMillis = additionalOptions! + .eagerRefreshThresholdMillis as number; + } + this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; + } + + /** + * Provides a mechanism to inject Downscoped access tokens directly. + * When the provided credential expires, a new credential, using the + * external account options are retrieved. + * Notice DownscopedClient is the broker class mainly used for generate + * downscoped access tokens, it is unlikely we call this function in real + * use case. + * We implement to make this a helper function for testing all cases in getAccessToken(). + * @param credentials The Credentials object to set on the current client. + */ + setCredentials(credentials: Credentials) { + super.setCredentials(credentials); + this.cachedDownscopedAccessToken = credentials; + } + + async getAccessToken(): Promise { + // If the cached access token is unavailable or expired, force refresh. + // The Downscoped access token will be returned in GetAccessTokenResponse format. + // If cached access token is unavailable or expired, force refresh. + if ( + !this.cachedDownscopedAccessToken || + this.isExpired(this.cachedDownscopedAccessToken) + ) { + await this.refreshAccessTokenAsync(); + } + // Return Downscoped access token in GetAccessTokenResponse format. + return { + token: this.cachedDownscopedAccessToken!.access_token, + res: this.cachedDownscopedAccessToken!.res, + }; + } + + /** + * The main authentication interface. It takes an optional url which when + * present is the endpoint> being accessed, and returns a Promise which + * resolves with authorization header fields. + * + * The result has the form: + * { Authorization: 'Bearer ' } + */ + async getRequestHeaders(): Promise { + throw new Error('Not implemented.'); + } + + /** + * Provides a request implementation with OAuth 2.0 flow. In cases of + * HTTP 401 and 403 responses, it automatically asks for a new access token + * and replays the unsuccessful request. + * @param opts Request options. + * @param callback callback. + * @return A promise that resolves with the HTTP response when no callback + * is provided. + */ + request(opts: GaxiosOptions): GaxiosPromise; + request(opts: GaxiosOptions, callback: BodyResponseCallback): void; + request( + opts: GaxiosOptions, + callback?: BodyResponseCallback + ): GaxiosPromise | void { + throw new Error('Not implemented.'); + } + + /** + * Forces token refresh, even if unexpired tokens are currently cached. + * GCP access tokens are retrieved from authclient object/source credential. + * Thenm GCP access tokens are exchanged for downscoped access tokens via the + * token exchange endpoint. + * @return A promise that resolves with the fresh downscoped access token. + */ + protected async refreshAccessTokenAsync(): Promise { + // Retrieve GCP access token from source credential. + const subjectToken = await (await this.authClient.getAccessToken()).token; + // Construct the STS credentials options. + const stsCredentialsOptions: sts.StsCredentialsOptions = { + grantType: STS_GRANT_TYPE, + requestedTokenType: STS_REQUEST_TOKEN_TYPE, + subjectToken: subjectToken as string, + subjectTokenType: STS_SUBJECT_TOKEN_TYPE, + }; + + // Exchange the source access token for a Downscoped access token. + const stsResponse = await this.stsCredential.exchangeToken( + stsCredentialsOptions, + undefined, + this.credentialAccessBoundary + ); + + // Save response in cached access token. + this.cachedDownscopedAccessToken = { + access_token: stsResponse.access_token, + expiry_date: new Date().getTime() + stsResponse.expires_in * 1000, + res: stsResponse.res, + }; + + // Save credentials. + this.credentials = {}; + Object.assign(this.credentials, this.cachedDownscopedAccessToken); + delete (this.credentials as CredentialsWithResponse).res; + + // Trigger tokens event to notify external listeners. + this.emit('tokens', { + refresh_token: null, + expiry_date: this.cachedDownscopedAccessToken!.expiry_date, + access_token: this.cachedDownscopedAccessToken!.access_token, + token_type: 'Bearer', + id_token: null, + }); + // Return the cached access token. + return this.cachedDownscopedAccessToken; + } + + /** + * Returns whether the provided credentials are expired or not. + * If there is no expiry time, assumes the token is not expired or expiring. + * @param downscopedAccessToken The credentials to check for expiration. + * @return Whether the credentials are expired or not. + */ + private isExpired(downscopedAccessToken: Credentials): boolean { + const now = new Date().getTime(); + return downscopedAccessToken.expiry_date + ? now >= + downscopedAccessToken.expiry_date - this.eagerRefreshThresholdMillis + : false; + } +} diff --git a/src/index.ts b/src/index.ts index 4642cf14..0062451e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ export { } from './auth/credentials'; export {GCPEnv} from './auth/envDetect'; export {GoogleAuthOptions, ProjectIdCallback} from './auth/googleauth'; +export {DownscopedClient} from './auth/downscopedclient'; export {IAMAuth, RequestMetadata} from './auth/iam'; export {IdTokenClient, IdTokenProvider} from './auth/idtokenclient'; export {Claims, JWTAccess} from './auth/jwtaccess'; diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 391616e2..71aec662 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -51,6 +51,7 @@ const poolId = 'POOL_ID'; const providerId = 'PROVIDER_ID'; const baseUrl = 'https://sts.googleapis.com'; const path = '/v1/token'; +const betaPath = '/v1beta/token'; const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; const saBaseUrl = 'https://iamcredentials.googleapis.com'; const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; @@ -75,6 +76,26 @@ export function mockStsTokenExchange( return scope; } +export function mockStsBetaTokenExchange( + nockParams: NockMockStsToken[] +): nock.Scope { + const scope = nock(baseUrl); + nockParams.forEach(nockMockStsToken => { + const headers = Object.assign( + { + 'content-type': 'application/x-www-form-urlencoded', + }, + nockMockStsToken.additionalHeaders || {} + ); + scope + .post(betaPath, qs.stringify(nockMockStsToken.request), { + reqheaders: headers, + }) + .reply(nockMockStsToken.statusCode, nockMockStsToken.response); + }); + return scope; +} + export function mockGenerateAccessToken( nockParams: NockMockGenerateAccessToken[] ): nock.Scope { diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts new file mode 100644 index 00000000..06d445b0 --- /dev/null +++ b/test/test.downscopedclient.ts @@ -0,0 +1,406 @@ +// Copyright 2021 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. + +import * as assert from 'assert'; +import {describe, it, before, after, beforeEach, afterEach} from 'mocha'; +import * as nock from 'nock'; +import * as sinon from 'sinon'; + +import {StsSuccessfulResponse} from '../src/auth/stscredentials'; +import {DownscopedClient} from '../src/auth/downscopedclient'; +import {AuthClient} from '../src/auth/authclient'; +import {mockStsBetaTokenExchange} from './externalclienthelper'; +import {GoogleAuth} from '../src'; + +nock.disableNetConnect(); + +interface SampleResponse { + foo: string; + bar: number; +} + +describe('DownscopedClient', () => { + let clock: sinon.SinonFakeTimers; + + const auth = new GoogleAuth({ + keyFilename: './test/fixtures/private.json', + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }); + let client: AuthClient; + + const ONE_HOUR_IN_SECS = 3600; + const testAvailableResource = + '//storage.googleapis.com/projects/_/buckets/bucket-123'; + const testAvailablePermission1 = 'inRole:roles/storage.objectViewer'; + const testAvailablePermission2 = 'inRole:roles/storage.objectAdmin'; + const testAvailabilityConditionExpression = + "resource.name.startsWith('projects/_/buckets/bucket-123/objects/prefix')"; + const testAvailabilityConditionTitle = 'Test availability condition title.'; + const testAvailabilityConditionDescription = + 'Test availability condition description.'; + const testClientAccessBoundary = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: testAvailableResource, + availablePermissions: [testAvailablePermission1], + availabilityCondition: { + expression: testAvailabilityConditionExpression, + }, + }, + ], + }, + }; + const stsSuccessfulResponse: StsSuccessfulResponse = { + access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN_0', + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: ONE_HOUR_IN_SECS, + scope: 'scope1 scope2', + }; + /** + * Offset to take into account network delays and server clock skews. + */ + const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000; + + beforeEach(async () => { + client = await auth.getClient(); + }); + + afterEach(() => { + nock.cleanAll(); + if (clock) { + clock.restore(); + } + }); + + describe('Constructor', () => { + it('should throw on empty access boundary rule', () => { + const expectedError = new Error( + 'At least one access boundary rule needs to be defined.' + ); + const cabWithEmptyAccessBoundaryRules = { + accessBoundary: { + accessBoundaryRules: [], + }, + }; + assert.throws(() => { + return new DownscopedClient(client, cabWithEmptyAccessBoundaryRules); + }, expectedError); + }); + + it('should throw on exceed number of access boundary rules', () => { + const expectedError = new Error( + 'Access boundary rule exceeds limit, max 10 allowed.' + ); + const cabWithExceedingAccessBoundaryRules = { + accessBoundary: { + accessBoundaryRules: [] as any, + }, + }; + const testAccessBoundaryRule = { + availableResource: testAvailableResource, + availablePermissions: [testAvailablePermission1], + availabilityCondition: { + expression: testAvailabilityConditionExpression, + }, + }; + for (let num = 0; num <= 10; num++) { + cabWithExceedingAccessBoundaryRules.accessBoundary.accessBoundaryRules.push( + testAccessBoundaryRule + ); + } + assert.throws(() => { + return new DownscopedClient( + client, + cabWithExceedingAccessBoundaryRules + ); + }, expectedError); + }); + + it('should throw on no permissions are defined in access boundary rules', () => { + const expectedError = new Error( + 'At least one permission should be defined in access boundary rules.' + ); + const cabWithNoPermissionIncluded = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: testAvailableResource, + availablePermissions: [], + availabilityCondition: { + expression: testAvailabilityConditionExpression, + }, + }, + ], + }, + }; + assert.throws(() => { + return new DownscopedClient(client, cabWithNoPermissionIncluded); + }, expectedError); + }); + + it('should not throw on one access boundary rule with all fields included', () => { + const cabWithOneAccessBoundaryRule = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: testAvailableResource, + availablePermissions: [testAvailablePermission1], + availabilityCondition: { + expression: testAvailabilityConditionExpression, + }, + }, + ], + }, + }; + assert.doesNotThrow(() => { + return new DownscopedClient(client, cabWithOneAccessBoundaryRule); + }); + }); + + it('should not throw with multiple permissions defined', () => { + const cabWithTwoAvailblePermissions = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: testAvailableResource, + availablePermissions: [ + testAvailablePermission1, + testAvailablePermission2, + ], + availabilityCondition: { + expression: testAvailabilityConditionExpression, + title: testAvailabilityConditionTitle, + description: testAvailabilityConditionDescription, + }, + }, + ], + }, + }; + assert.doesNotThrow(() => { + return new DownscopedClient(client, cabWithTwoAvailblePermissions); + }); + }); + + it('should not throw with empty available condition', () => { + const cabWithNoAvailabilityCondition = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: testAvailableResource, + availablePermissions: [testAvailablePermission1], + }, + ], + }, + }; + assert.doesNotThrow(() => { + return new DownscopedClient(client, cabWithNoAvailabilityCondition); + }); + }); + + it('should not throw with only expression setup in available condition', () => { + const cabWithOnlyAvailabilityConditionExpression = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: testAvailableResource, + availablePermissions: [ + testAvailablePermission1, + testAvailablePermission2, + ], + availabilityCondition: { + expression: testAvailabilityConditionExpression, + }, + }, + ], + }, + }; + assert.doesNotThrow(() => { + return new DownscopedClient( + client, + cabWithOnlyAvailabilityConditionExpression + ); + }); + }); + + it('should set custom RefreshOptions', () => { + const refreshOptions = { + eagerRefreshThresholdMillis: 5000, + forceRefreshOnFailure: true, + }; + const cabWithOneAccessBoundaryRules = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: testAvailableResource, + availablePermissions: [testAvailablePermission1], + availabilityCondition: { + expression: testAvailabilityConditionExpression, + }, + }, + ], + }, + }; + const downscopedClient = new DownscopedClient( + client, + cabWithOneAccessBoundaryRules, + refreshOptions + ); + assert.strictEqual( + downscopedClient.forceRefreshOnFailure, + refreshOptions.forceRefreshOnFailure + ); + assert.strictEqual( + downscopedClient.eagerRefreshThresholdMillis, + refreshOptions.eagerRefreshThresholdMillis + ); + }); + }); + + describe('getAccessToken()', () => { + let sandbox: sinon.SinonSandbox; + before(() => { + const expectedAccessTokenResponse = { + token: 'subject_token', + }; + sandbox = sinon.createSandbox(); + sandbox + .stub(client, 'getAccessToken') + .resolves(expectedAccessTokenResponse); + }); + + after(() => { + sandbox.restore(); + }); + + it('should return current unexpired cached DownscopedClient access token', async () => { + const now = new Date().getTime(); + const credentials = { + access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', + expiry_date: now + ONE_HOUR_IN_SECS * 1000, + }; + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + downscopedClient.setCredentials(credentials); + const tokenResponse = await downscopedClient.getAccessToken(); + assert.deepStrictEqual(tokenResponse.token, credentials.access_token); + }); + + it('should refresh a new DownscopedClient access when cached one gets expired', async () => { + const now = new Date().getTime(); + clock = sinon.useFakeTimers(now); + const credentials = { + access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', + expiry_date: now + ONE_HOUR_IN_SECS * 1000, + }; + const scope = mockStsBetaTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: + testClientAccessBoundary && + JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + downscopedClient.setCredentials(credentials); + + clock.tick(ONE_HOUR_IN_SECS * 1000 - EXPIRATION_TIME_OFFSET - 1); + const tokenResponse = await downscopedClient.getAccessToken(); + assert.deepStrictEqual(tokenResponse.token, credentials.access_token); + + clock.tick(1); + const refreshedTokenResponse = await downscopedClient.getAccessToken(); + + assert.deepStrictEqual( + refreshedTokenResponse.token, + stsSuccessfulResponse.access_token + ); + scope.done(); + }); + + it('should return new DownscopedClient access token when there is no cached downscoped access token', async () => { + const scope = mockStsBetaTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: + testClientAccessBoundary && + JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + const tokenResponse = await downscopedClient.getAccessToken(); + + assert.deepStrictEqual( + tokenResponse.token, + stsSuccessfulResponse.access_token + ); + scope.done(); + }); + }); + + describe('getRequestHeader()', () => { + it('should return unimplemented error when calling getRequestHeader()', async () => { + const expectedError = new Error('Not implemented.'); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + await assert.rejects(cabClient.getRequestHeaders(), expectedError); + }); + }); + + describe('request()', () => { + it('should return unimplemented error when request with opts', () => { + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const expectedError = new Error('Not implemented.'); + + assert.throws(() => { + return cabClient.request({ + url: 'https://example.com/api', + method: 'POST', + data: exampleRequest, + responseType: 'json', + }); + }, expectedError); + }); + }); +}); diff --git a/test/test.index.ts b/test/test.index.ts index 06bb8190..5945efd0 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -42,5 +42,6 @@ describe('index', () => { assert(gal.IdentityPoolClient); assert(gal.AwsClient); assert(gal.BaseExternalAccountClient); + assert(gal.DownscopedClient); }); }); From f8dcb0ff592cc1f2f97d0d49bdabf0c2b41a0d3d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 30 Jun 2021 21:22:19 +0000 Subject: [PATCH 261/662] chore: release 7.2.0 (#1203) :robot: I have created a release \*beep\* \*boop\* --- ## [7.2.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.2...v7.2.0) (2021-06-30) ### Features * Implement DownscopedClient#getAccessToken() and unit test ([#1201](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1201)) ([faa6677](https://www.github.com/googleapis/google-auth-library-nodejs/commit/faa6677fe72c8fc671a2190abe45897ac58cc42e)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf5a726..0e8252d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.2.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.2...v7.2.0) (2021-06-30) + + +### Features + +* Implement DownscopedClient#getAccessToken() and unit test ([#1201](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1201)) ([faa6677](https://www.github.com/googleapis/google-auth-library-nodejs/commit/faa6677fe72c8fc671a2190abe45897ac58cc42e)) + ### [7.1.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.1...v7.1.2) (2021-06-10) diff --git a/package.json b/package.json index c3472e59..3839a023 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.1.2", + "version": "7.2.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 7512d147..214734fa 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.1.2", + "google-auth-library": "^7.2.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 79e100e9ddc64f34e34d0e91c8188f1818e33a1c Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Fri, 2 Jul 2021 15:32:37 -0400 Subject: [PATCH 262/662] feat: add useJWTAccessAlways and defaultServicePath variable (#1204) --- .gitignore | 1 + src/auth/googleauth.ts | 18 ++++++++++++++---- src/auth/jwtclient.ts | 3 ++- test/test.googleauth.ts | 13 +++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index bb359c02..ca374304 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ package-lock.json yarn.lock dist/ __pycache__ +.DS_Store diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 6b84e6f6..8a4b0c7a 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -111,6 +111,8 @@ export class GoogleAuth { * @private */ private checkIsGCE?: boolean = undefined; + useJWTAccessAlways?: boolean; + defaultServicePath?: string; // Note: this properly is only public to satisify unit tests. // https://github.com/Microsoft/TypeScript/issues/5228 @@ -150,6 +152,15 @@ export class GoogleAuth { this.clientOptions = opts.clientOptions; } + // 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: JWT) { + client.defaultServicePath = this.defaultServicePath; + client.useJWTAccessAlways = this.useJWTAccessAlways; + client.defaultScopes = this.defaultScopes; + } + /** * Obtains the default project ID for the application. * @param callback Optional callback @@ -265,7 +276,6 @@ export class GoogleAuth { await this._tryGetApplicationCredentialsFromEnvironmentVariable(options); if (credential) { if (credential instanceof JWT) { - credential.defaultScopes = this.defaultScopes; credential.scopes = this.scopes; } else if (credential instanceof BaseExternalAccountClient) { credential.scopes = this.getAnyScopes(); @@ -281,7 +291,6 @@ export class GoogleAuth { ); if (credential) { if (credential instanceof JWT) { - credential.defaultScopes = this.defaultScopes; credential.scopes = this.scopes; } else if (credential instanceof BaseExternalAccountClient) { credential.scopes = this.getAnyScopes(); @@ -456,7 +465,7 @@ export class GoogleAuth { } else { (options as JWTOptions).scopes = this.scopes; client = new JWT(options); - client.defaultScopes = this.defaultScopes; + this.setGapicJWTValues(client); client.fromJSON(json); } return client; @@ -488,7 +497,7 @@ export class GoogleAuth { } else { (options as JWTOptions).scopes = this.scopes; client = new JWT(options); - client.defaultScopes = this.defaultScopes; + this.setGapicJWTValues(client); client.fromJSON(json); } // cache both raw data used to instantiate client and client itself. @@ -564,6 +573,7 @@ export class GoogleAuth { keyFile: this.keyFilename, }); this.cachedCredential = client; + this.setGapicJWTValues(client); return resolve(client); } } catch (err) { diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 38d94a11..34483114 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -46,7 +46,8 @@ export class JWT extends OAuth2Client implements IdTokenProvider { subject?: string; gtoken?: GoogleToken; additionalClaims?: {}; - + useJWTAccessAlways?: boolean; + defaultServicePath?: string; private access?: JWTAccess; /** diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 0a11628e..a08a98af 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -407,6 +407,19 @@ describe('googleauth', () => { const result = auth.fromJSON(json); assert.strictEqual(undefined, (result as JWT).scopes); }); + it('fromJSON should set useJWTAccessAlways with private key', () => { + auth.useJWTAccessAlways = true; + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.ok((result as JWT).useJWTAccessAlways); + }); + + it('fromJSON should set default service path with private key', () => { + auth.defaultServicePath = 'a/b/c'; + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.strictEqual((result as JWT).defaultServicePath, 'a/b/c'); + }); it('fromJSON should create JWT with null subject', () => { const json = createJwtJSON(); From dd2d07a01daf9d7e766189b889fb36a9f6fb8e73 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 7 Jul 2021 07:45:41 -0700 Subject: [PATCH 263/662] chore: release 7.3.0 (#1205) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8252d5..74400043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.3.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.2.0...v7.3.0) (2021-07-02) + + +### Features + +* add useJWTAccessAlways and defaultServicePath variable ([#1204](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1204)) ([79e100e](https://www.github.com/googleapis/google-auth-library-nodejs/commit/79e100e9ddc64f34e34d0e91c8188f1818e33a1c)) + ## [7.2.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.2...v7.2.0) (2021-06-30) diff --git a/package.json b/package.json index 3839a023..263425d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.2.0", + "version": "7.3.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 214734fa..0389b5cb 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.2.0", + "google-auth-library": "^7.3.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From bbcc9d3daf3822700120220c1daff26a2e709894 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 21 Jul 2021 00:04:20 +0000 Subject: [PATCH 264/662] build: switch to release-please release tagging (#1129) (#1214) Requires https://github.com/googleapis/releasetool/pull/338 Source-Link: https://github.com/googleapis/synthtool/commit/1563597d28eca099d6411bbc29ecd09314a80746 Post-Processor: gcr.io/repo-automation-bots/owlbot-nodejs:latest@sha256:06c970a44680229c1e8cefa701dbc93b80468ec4a34e6968475084e4ec1e2d7d --- .github/.OwlBot.lock.yaml | 2 +- .github/release-please.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 9d507eee..b1434427 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - digest: sha256:41d5457ff79c3945782ab7e23bf4d617fd7bf3f2b03b6d84808010f7d2e10ca2 + digest: sha256:06c970a44680229c1e8cefa701dbc93b80468ec4a34e6968475084e4ec1e2d7d diff --git a/.github/release-please.yml b/.github/release-please.yml index 85344b92..a1b41da3 100644 --- a/.github/release-please.yml +++ b/.github/release-please.yml @@ -1 +1,2 @@ +handleGHRelease: true releaseType: node From 3e0c4c92014f2b7b4543f28ad352f0d135efd08f Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 23 Jul 2021 14:14:38 -0400 Subject: [PATCH 265/662] meta: add nodejs-auth as codeowner (#1217) --- .github/CODEOWNERS | 2 +- .repo-metadata.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 80520bba..93cb5059 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,7 +6,7 @@ # The yoshi-nodejs team is the default owner for nodejs repositories. -* @googleapis/yoshi-nodejs +* @googleapis/yoshi-nodejs @googleapis/nodejs-auth # The github automation team is the default owner for the auto-approve file. .github/auto-approve.yml @googleapis/github-automation diff --git a/.repo-metadata.json b/.repo-metadata.json index e6ca9104..a99de37d 100644 --- a/.repo-metadata.json +++ b/.repo-metadata.json @@ -6,5 +6,6 @@ "release_level": "ga", "language": "nodejs", "repo": "googleapis/google-auth-library-nodejs", - "distribution_name": "google-auth-library" -} \ No newline at end of file + "distribution_name": "google-auth-library", + "codeowner_team": "@googleapis/nodejs-auth" +} From 804a430a2f2661382b4d874335bc540df15eaf62 Mon Sep 17 00:00:00 2001 From: Averi Kitsch Date: Mon, 26 Jul 2021 13:33:35 -0700 Subject: [PATCH 266/662] samples: update TODO section (#1218) * fix: update TODO section * remove origin * Update idtokens-serverless.js --- samples/idtokens-serverless.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index c76a8605..1bcc85e0 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -29,6 +29,7 @@ function main( * TODO(developer): Uncomment these variables before running the sample. */ // const url = 'https://TARGET_URL'; + // let targetAudience = null; const {GoogleAuth} = require('google-auth-library'); const auth = new GoogleAuth(); @@ -36,7 +37,7 @@ function main( if (!targetAudience) { // Use the request URL hostname as the target audience for requests. const {URL} = require('url'); - targetAudience = new URL(url).origin; + targetAudience = new URL(url); } console.info(`request ${url} with target audience ${targetAudience}`); const client = await auth.getIdTokenClient(targetAudience); From ab1cd31e07d45424f614e0401d1068df2fbd914c Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Thu, 29 Jul 2021 10:00:32 -0400 Subject: [PATCH 267/662] feat(impersonated): add impersonated credentials auth (#1207) --- .readme-partials.yaml | 70 ++++++++ README.md | 69 ++++++++ src/auth/googleauth.ts | 16 +- src/auth/impersonated.ts | 146 ++++++++++++++++ src/index.ts | 1 + test/test.impersonated.ts | 351 ++++++++++++++++++++++++++++++++++++++ test/test.index.ts | 1 + 7 files changed, 650 insertions(+), 4 deletions(-) create mode 100644 src/auth/impersonated.ts create mode 100644 test/test.impersonated.ts diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 5472a003..3621ce5d 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -8,6 +8,7 @@ body: |- - [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication. - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). + - [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account. ## Application Default Credentials This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. @@ -606,3 +607,72 @@ body: |- ``` A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). + + ## Impersonated Credentials Client + + Google Cloud Impersonated credentials used for [Creating short-lived service account credentials](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials). + + Provides authentication for applications where local credentials impersonates a remote service account using [IAM Credentials API](https://cloud.google.com/iam/docs/reference/credentials/rest). + + An Impersonated Credentials Client is instantiated with a `sourceClient`. This + client should use credentials that have the "Service Account Token Creator" role (`roles/iam.serviceAccountTokenCreator`), + and should authenticate with the `https://www.googleapis.com/auth/cloud-platform`, or `https://www.googleapis.com/auth/iam` scopes. + + `sourceClient` is used by the Impersonated + Credentials Client to impersonate a target service account with a specified + set of scopes. + + ### Sample Usage + + ```javascript + const { GoogleAuth, Impersonated } = require('google-auth-library'); + const { SecretManagerServiceClient } = require('@google-cloud/secret-manager'); + + async function main() { + + // Acquire source credentials: + const auth = new GoogleAuth(); + const client = await auth.getClient(); + + // Impersonate new credentials: + let targetClient = new Impersonated({ + sourceClient: client, + targetPrincipal: 'impersonated-account@projectID.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'] + }); + + // Get impersonated credentials: + const authHeaders = await targetClient.getRequestHeaders(); + // Do something with `authHeaders.Authorization`. + + // Use impersonated credentials: + const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID' + const resp = await targetClient.request({ url }); + for (const bucket of resp.data.items) { + console.log(bucket.name); + } + + // Use impersonated credentials with google-cloud client library + // Note: this works only with certain cloud client libraries utilizing gRPC + // e.g., SecretManager, KMS, AIPlatform + // will not currently work with libraries using REST, e.g., Storage, Compute + const smClient = new SecretManagerServiceClient({ + projectId: anotherProjectID, + auth: { + getClient: () => targetClient, + }, + }); + const secretName = 'projects/anotherProjectNumber/secrets/someProjectName/versions/1'; + const [accessResponse] = await smClient.accessSecretVersion({ + name: secretName, + }); + + const responsePayload = accessResponse.payload.data.toString('utf8'); + // Do something with the secret contained in `responsePayload`. + }; + + main(); + ``` + diff --git a/README.md b/README.md index 375f7bbc..19a4de8b 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ This library provides a variety of ways to authenticate to your Google services. - [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication. - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). +- [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account. ## Application Default Credentials This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. @@ -652,6 +653,74 @@ console.log(ticket) A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). +## Impersonated Credentials Client + +Google Cloud Impersonated credentials used for [Creating short-lived service account credentials](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials). + +Provides authentication for applications where local credentials impersonates a remote service account using [IAM Credentials API](https://cloud.google.com/iam/docs/reference/credentials/rest). + +An Impersonated Credentials Client is instantiated with a `sourceClient`. This +client should use credentials that have the "Service Account Token Creator" role (`roles/iam.serviceAccountTokenCreator`), +and should authenticate with the `https://www.googleapis.com/auth/cloud-platform`, or `https://www.googleapis.com/auth/iam` scopes. + +`sourceClient` is used by the Impersonated +Credentials Client to impersonate a target service account with a specified +set of scopes. + +### Sample Usage + +```javascript +const { GoogleAuth, Impersonated } = require('google-auth-library'); +const { SecretManagerServiceClient } = require('@google-cloud/secret-manager'); + +async function main() { + + // Acquire source credentials: + const auth = new GoogleAuth(); + const client = await auth.getClient(); + + // Impersonate new credentials: + let targetClient = new Impersonated({ + sourceClient: client, + targetPrincipal: 'impersonated-account@projectID.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'] + }); + + // Get impersonated credentials: + const authHeaders = await targetClient.getRequestHeaders(); + // Do something with `authHeaders.Authorization`. + + // Use impersonated credentials: + const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID' + const resp = await targetClient.request({ url }); + for (const bucket of resp.data.items) { + console.log(bucket.name); + } + + // Use impersonated credentials with google-cloud client library + // Note: this works only with certain cloud client libraries utilizing gRPC + // e.g., SecretManager, KMS, AIPlatform + // will not currently work with libraries using REST, e.g., Storage, Compute + const smClient = new SecretManagerServiceClient({ + projectId: anotherProjectID, + auth: { + getClient: () => targetClient, + }, + }); + const secretName = 'projects/anotherProjectNumber/secrets/someProjectName/versions/1'; + const [accessResponse] = await smClient.accessSecretVersion({ + name: secretName, + }); + + const responsePayload = accessResponse.payload.data.toString('utf8'); + // Do something with the secret contained in `responsePayload`. +}; + +main(); +``` + ## Samples diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 8a4b0c7a..1b037173 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -30,6 +30,7 @@ import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; import {Headers, OAuth2ClientOptions, RefreshOptions} from './oauth2client'; import {UserRefreshClient, UserRefreshClientOptions} from './refreshclient'; +import {Impersonated, ImpersonatedOptions} from './impersonated'; import { ExternalAccountClient, ExternalAccountClientOptions, @@ -44,7 +45,11 @@ import {AuthClient} from './authclient'; * Defines all types of explicit clients that are determined via ADC JSON * config file. */ -export type JSONClient = JWT | UserRefreshClient | BaseExternalAccountClient; +export type JSONClient = + | JWT + | UserRefreshClient + | BaseExternalAccountClient + | Impersonated; export interface ProjectIdCallback { (err?: Error | null, projectId?: string | null): void; @@ -86,7 +91,11 @@ export interface GoogleAuthOptions { /** * Options object passed to the constructor of the client */ - clientOptions?: JWTOptions | OAuth2ClientOptions | UserRefreshClientOptions; + clientOptions?: + | JWTOptions + | OAuth2ClientOptions + | UserRefreshClientOptions + | ImpersonatedOptions; /** * Required scopes for the desired API request @@ -126,14 +135,13 @@ export class GoogleAuth { // To save the contents of the JSON credential file jsonContent: JWTInput | ExternalAccountClientOptions | null = null; - cachedCredential: JSONClient | Compute | null = null; + cachedCredential: JSONClient | Impersonated | Compute | null = null; /** * Scopes populated by the client library by default. We differentiate between * these and user defined scopes when deciding whether to use a self-signed JWT. */ defaultScopes?: string | string[]; - private keyFilename?: string; private scopes?: string | string[]; private clientOptions?: RefreshOptions; diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts new file mode 100644 index 00000000..a6646095 --- /dev/null +++ b/src/auth/impersonated.ts @@ -0,0 +1,146 @@ +/** + * Copyright 2021 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. + */ + +import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; +import {AuthClient} from './authclient'; + +export interface ImpersonatedOptions extends RefreshOptions { + /** + * Client used to perform exchange for impersonated client. + */ + sourceClient?: AuthClient; + /** + * The service account to impersonate. + */ + targetPrincipal?: string; + /** + * Scopes to request during the authorization grant. + */ + targetScopes?: string[]; + /** + * The chained list of delegates required to grant the final access_token. + */ + delegates?: string[]; + /** + * Number of seconds the delegated credential should be valid. + */ + lifetime?: number | 3600; + /** + * API endpoint to fetch token from. + */ + endpoint?: string; +} + +export interface TokenResponse { + accessToken: string; + expireTime: string; +} + +export class Impersonated extends OAuth2Client { + private sourceClient: AuthClient; + private targetPrincipal: string; + private targetScopes: string[]; + private delegates: string[]; + private lifetime: number; + private endpoint: string; + + /** + * Impersonated service account credentials. + * + * Create a new access token by impersonating another service account. + * + * Impersonated Credentials allowing credentials issued to a user or + * service account to impersonate another. The source project using + * Impersonated Credentials must enable the "IAMCredentials" API. + * Also, the target service account must grant the orginating principal + * the "Service Account Token Creator" IAM role. + * + * @param {object} options - The configuration object. + * @param {object} [options.sourceClient] the source credential used as to + * acquire the impersonated credentials. + * @param {string} [options.targetPrincipal] the service account to + * impersonate. + * @param {string[]} [options.delegates] the chained list of delegates + * required to grant the final access_token. If set, the sequence of + * identities must have "Service Account Token Creator" capability granted to + * the preceding identity. For example, if set to [serviceAccountB, + * serviceAccountC], the sourceCredential must have the Token Creator role on + * serviceAccountB. serviceAccountB must have the Token Creator on + * serviceAccountC. Finally, C must have Token Creator on target_principal. + * If left unset, sourceCredential must have that role on targetPrincipal. + * @param {string[]} [options.targetScopes] scopes to request during the + * authorization grant. + * @param {number} [options.lifetime] number of seconds the delegated + * credential should be valid for up to 3600 seconds by default, or 43,200 + * seconds by extending the token's lifetime, see: + * https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth + * @param {string} [options.endpoint] api endpoint override. + */ + constructor(options: ImpersonatedOptions = {}) { + super(options); + this.credentials = { + expiry_date: 1, + refresh_token: 'impersonated-placeholder', + }; + this.sourceClient = options.sourceClient ?? new OAuth2Client(); + this.targetPrincipal = options.targetPrincipal ?? ''; + this.delegates = options.delegates ?? []; + this.targetScopes = options.targetScopes ?? []; + this.lifetime = options.lifetime ?? 3600; + this.endpoint = options.endpoint ?? 'https://iamcredentials.googleapis.com'; + } + + /** + * Refreshes the access token. + * @param refreshToken Unused parameter + */ + protected async refreshToken( + refreshToken?: string | null + ): Promise { + try { + await this.sourceClient.getAccessToken(); + const name = 'projects/-/serviceAccounts/' + this.targetPrincipal; + const u = `${this.endpoint}/v1/${name}:generateAccessToken`; + const body = { + delegates: this.delegates, + scope: this.targetScopes, + lifetime: this.lifetime + 's', + }; + const res = await this.sourceClient.request({ + url: u, + data: body, + method: 'POST', + }); + const tokenResponse = res.data; + this.credentials.access_token = tokenResponse.accessToken; + this.credentials.expiry_date = Date.parse(tokenResponse.expireTime); + return { + tokens: this.credentials, + res, + }; + } catch (error) { + const status = error?.response?.data?.error?.status; + const message = error?.response?.data?.error?.message; + if (status && message) { + error.message = `${status}: unable to impersonate: ${message}`; + throw error; + } else { + error.message = `unable to impersonate: ${error}`; + throw error; + } + } + } +} diff --git a/src/index.ts b/src/index.ts index 0062451e..a0fa19ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ export {IAMAuth, RequestMetadata} from './auth/iam'; export {IdTokenClient, IdTokenProvider} from './auth/idtokenclient'; export {Claims, JWTAccess} from './auth/jwtaccess'; export {JWT, JWTOptions} from './auth/jwtclient'; +export {Impersonated, ImpersonatedOptions} from './auth/impersonated'; export { Certificates, CodeChallengeMethod, diff --git a/test/test.impersonated.ts b/test/test.impersonated.ts new file mode 100644 index 00000000..9f2a9d8d --- /dev/null +++ b/test/test.impersonated.ts @@ -0,0 +1,351 @@ +/** + * Copyright 2021 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. + */ + +import * as assert from 'assert'; +import * as nock from 'nock'; +import {describe, it, afterEach} from 'mocha'; +import {Impersonated, JWT} from '../src'; +import {CredentialRequest} from '../src/auth/credentials'; + +const PEM_PATH = './test/fixtures/private.pem'; + +nock.disableNetConnect(); + +const url = 'http://example.com'; + +function createGTokenMock(body: CredentialRequest) { + return nock('https://www.googleapis.com') + .post('/oauth2/v4/token') + .reply(200, body); +} + +interface ImpersonatedCredentialRequest { + delegates: string[]; + scope: string[]; + lifetime: string; +} + +describe('impersonated', () => { + afterEach(() => { + nock.cleanAll(); + }); + + it('should request impersonated credentials on first request', async () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const scopes = [ + nock(url).get('/').reply(200), + createGTokenMock({ + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', + (body: ImpersonatedCredentialRequest) => { + assert.strictEqual(body.lifetime, '30s'); + assert.deepStrictEqual(body.delegates, []); + assert.deepStrictEqual(body.scope, [ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + return true; + } + ) + .reply(200, { + accessToken: 'qwerty345', + expireTime: tomorrow.toISOString(), + }), + ]; + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' + ); + const impersonated = new Impersonated({ + sourceClient: jwt, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + await impersonated.request({url}); + assert.strictEqual(impersonated.credentials.access_token, 'qwerty345'); + assert.strictEqual( + impersonated.credentials.expiry_date, + tomorrow.getTime() + ); + scopes.forEach(s => s.done()); + }); + + it('should not request impersonated credentials on second request', async () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const scopes = [ + nock(url).get('/').reply(200), + nock(url).get('/').reply(200), + createGTokenMock({ + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', + (body: ImpersonatedCredentialRequest) => { + assert.strictEqual(body.lifetime, '30s'); + assert.deepStrictEqual(body.delegates, []); + assert.deepStrictEqual(body.scope, [ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + return true; + } + ) + .reply(200, { + accessToken: 'qwerty345', + expireTime: tomorrow.toISOString(), + }), + ]; + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' + ); + const impersonated = new Impersonated({ + sourceClient: jwt, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + await impersonated.request({url}); + await impersonated.request({url}); + assert.strictEqual(impersonated.credentials.access_token, 'qwerty345'); + assert.strictEqual( + impersonated.credentials.expiry_date, + tomorrow.getTime() + ); + scopes.forEach(s => s.done()); + }); + + it('should request impersonated credentials once new credentials expire', async () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const scopes = [ + nock(url).get('/').reply(200), + nock(url).get('/').reply(200), + createGTokenMock({ + access_token: 'abc123', + }), + createGTokenMock({ + access_token: 'abc456', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', + () => { + return true; + } + ) + .reply(200, { + accessToken: 'qwerty345', + expireTime: tomorrow.toISOString(), + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', + () => { + return true; + } + ) + .reply(200, { + accessToken: 'qwerty456', + expireTime: tomorrow.toISOString(), + }), + ]; + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' + ); + const impersonated = new Impersonated({ + sourceClient: jwt, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + await impersonated.request({url}); + // Force both the wrapped and impersonated client to appear to have + // expired: + jwt.credentials.expiry_date = Date.now(); + impersonated.credentials.expiry_date = Date.now(); + await impersonated.request({url}); + assert.strictEqual(impersonated.credentials.access_token, 'qwerty456'); + assert.strictEqual( + impersonated.credentials.expiry_date, + tomorrow.getTime() + ); + scopes.forEach(s => s.done()); + }); + + it('throws meaningful error when context available', async () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const scopes = [ + createGTokenMock({ + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken' + ) + .reply(404, { + error: { + code: 404, + message: 'Requested entity was not found.', + status: 'NOT_FOUND', + }, + }), + ]; + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' + ); + const impersonated = new Impersonated({ + sourceClient: jwt, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + impersonated.credentials.access_token = 'initial-access-token'; + impersonated.credentials.expiry_date = Date.now() - 10000; + await assert.rejects(impersonated.request({url}), /NOT_FOUND/); + scopes.forEach(s => s.done()); + }); + + it('handles errors without context', async () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const scopes = [ + createGTokenMock({ + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken' + ) + .reply(500), + ]; + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' + ); + const impersonated = new Impersonated({ + sourceClient: jwt, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + impersonated.credentials.access_token = 'initial-access-token'; + impersonated.credentials.expiry_date = Date.now() - 10000; + await assert.rejects(impersonated.request({url}), /unable to impersonate/); + scopes.forEach(s => s.done()); + }); + + it('handles error authenticating sourceClient', async () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const scopes = [ + nock('https://www.googleapis.com').post('/oauth2/v4/token').reply(401), + ]; + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' + ); + const impersonated = new Impersonated({ + sourceClient: jwt, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + await assert.rejects(impersonated.request({url}), /unable to impersonate/); + scopes.forEach(s => s.done()); + }); + + it('should populate request headers', async () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const scopes = [ + createGTokenMock({ + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', + (body: ImpersonatedCredentialRequest) => { + assert.strictEqual(body.lifetime, '30s'); + assert.deepStrictEqual(body.delegates, []); + assert.deepStrictEqual(body.scope, [ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + return true; + } + ) + .reply(200, { + accessToken: 'qwerty345', + expireTime: tomorrow.toISOString(), + }), + ]; + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' + ); + const impersonated = new Impersonated({ + sourceClient: jwt, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + impersonated.credentials.access_token = 'initial-access-token'; + impersonated.credentials.expiry_date = Date.now() - 10000; + const headers = await impersonated.getRequestHeaders(); + assert.strictEqual(headers['Authorization'], 'Bearer qwerty345'); + assert.strictEqual( + impersonated.credentials.expiry_date, + tomorrow.getTime() + ); + scopes.forEach(s => s.done()); + }); +}); diff --git a/test/test.index.ts b/test/test.index.ts index 5945efd0..822eda1c 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -43,5 +43,6 @@ describe('index', () => { assert(gal.AwsClient); assert(gal.BaseExternalAccountClient); assert(gal.DownscopedClient); + assert(gal.Impersonated); }); }); From 1578f23dc8c31a4116be84687606d4e2605ca4e8 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 29 Jul 2021 14:08:25 +0000 Subject: [PATCH 268/662] chore: release 7.4.0 (#1220) :robot: I have created a release \*beep\* \*boop\* --- ## [7.4.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.3.0...v7.4.0) (2021-07-29) ### Features * **impersonated:** add impersonated credentials auth ([#1207](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1207)) ([ab1cd31](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ab1cd31e07d45424f614e0401d1068df2fbd914c)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74400043..cff66d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.4.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.3.0...v7.4.0) (2021-07-29) + + +### Features + +* **impersonated:** add impersonated credentials auth ([#1207](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1207)) ([ab1cd31](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ab1cd31e07d45424f614e0401d1068df2fbd914c)) + ## [7.3.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.2.0...v7.3.0) (2021-07-02) diff --git a/package.json b/package.json index 263425d3..a2e6400e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.3.0", + "version": "7.4.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 0389b5cb..aa5f5bb6 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.3.0", + "google-auth-library": "^7.4.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 4fbe67e08bce3f31193d3bb7b93c4cc1251e66a2 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Thu, 29 Jul 2021 08:34:02 -0700 Subject: [PATCH 269/662] fix(downscoped-client): bug fixes for downscoped client implementation. (#1219) --- src/auth/baseexternalclient.ts | 2 +- src/auth/downscopedclient.ts | 120 +++++++++++----- src/auth/stscredentials.ts | 2 +- src/index.ts | 1 - test/externalclienthelper.ts | 21 --- test/test.downscopedclient.ts | 250 +++++++++++++++++++++++++++------ test/test.index.ts | 1 - 7 files changed, 294 insertions(+), 103 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index ea33f4e9..af93a74d 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -215,7 +215,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { /** * The main authentication interface. It takes an optional url which when - * present is the endpoint> being accessed, and returns a Promise which + * present is the endpoint being accessed, and returns a Promise which * resolves with authorization header fields. * * The result has the form: diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index b7768cc8..bbd3d147 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -34,7 +34,13 @@ const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; */ const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; /** The STS access token exchange end point. */ -const STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1beta/token'; +const STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1/token'; + +/** + * The maximum number of access boundary rules a Credential Access Boundary + * can contain. + */ +export const MAX_ACCESS_BOUNDARY_RULES_COUNT = 10; /** * Offset to take into account network delays and server clock skews. @@ -48,10 +54,18 @@ interface CredentialsWithResponse extends Credentials { res?: GaxiosResponse | null; } +/** + * Internal interface for tracking and returning the Downscoped access token + * expiration time in epoch time (seconds). + */ +interface DownscopedAccessTokenResponse extends GetAccessTokenResponse { + expirationTime?: number | null; +} + /** * Defines an upper bound of permissions available for a GCP credential. */ -interface CredentialAccessBoundary { +export interface CredentialAccessBoundary { accessBoundary: { accessBoundaryRules: AccessBoundaryRule[]; }; @@ -74,35 +88,67 @@ interface AvailabilityCondition { description?: string; } +/** + * Defines a set of Google credentials that are downscoped from an existing set + * of Google OAuth2 credentials. This is useful to restrict the Identity and + * Access Management (IAM) permissions that a short-lived credential can use. + * The common pattern of usage is to have a token broker with elevated access + * generate these downscoped credentials from higher access source credentials + * and pass the downscoped short-lived access tokens to a token consumer via + * some secure authenticated channel for limited access to Google Cloud Storage + * resources. + */ export class DownscopedClient extends AuthClient { - /** - * OAuth scopes for the GCP access token to use. When not provided, - * the default https://www.googleapis.com/auth/cloud-platform is - * used. - */ private cachedDownscopedAccessToken: CredentialsWithResponse | null; private readonly stsCredential: sts.StsCredentials; - public readonly authClient: AuthClient; - public readonly credentialAccessBoundary: CredentialAccessBoundary; public readonly eagerRefreshThresholdMillis: number; public readonly forceRefreshOnFailure: boolean; + /** + * Instantiates a downscoped client object using the provided source + * AuthClient and credential access boundary rules. + * To downscope permissions of a source AuthClient, a Credential Access + * Boundary that specifies which resources the new credential can access, as + * well as an upper bound on the permissions that are available on each + * resource, has to be defined. A downscoped client can then be instantiated + * using the source AuthClient and the Credential Access Boundary. + * @param authClient The source AuthClient to be downscoped based on the + * provided Credential Access Boundary rules. + * @param credentialAccessBoundary The Credential Access Boundary which + * contains a list of access boundary rules. Each rule contains information + * on the resource that the rule applies to, the upper bound of the + * permissions that are available on that resource and an optional + * condition to further restrict permissions. + * @param additionalOptions Optional additional behavior customization + * options. These currently customize expiration threshold time and + * whether to retry on 401/403 API request errors. + */ constructor( - private client: AuthClient, - private cab: CredentialAccessBoundary, + private readonly authClient: AuthClient, + private readonly credentialAccessBoundary: CredentialAccessBoundary, additionalOptions?: RefreshOptions ) { super(); - - // Check a number of 1-10 access boundary rules are defined within credential access boundary. - if (cab.accessBoundary.accessBoundaryRules.length === 0) { + // Check 1-10 Access Boundary Rules are defined within Credential Access + // Boundary. + if ( + credentialAccessBoundary.accessBoundary.accessBoundaryRules.length === 0 + ) { throw new Error('At least one access boundary rule needs to be defined.'); - } else if (cab.accessBoundary.accessBoundaryRules.length > 10) { - throw new Error('Access boundary rule exceeds limit, max 10 allowed.'); + } else if ( + credentialAccessBoundary.accessBoundary.accessBoundaryRules.length > + MAX_ACCESS_BOUNDARY_RULES_COUNT + ) { + throw new Error( + 'The provided access boundary has more than ' + + `${MAX_ACCESS_BOUNDARY_RULES_COUNT} access boundary rules.` + ); } - // Check at least one permission should be defined in each access boundary rule. - for (const rule of cab.accessBoundary.accessBoundaryRules) { + // Check at least one permission should be defined in each Access Boundary + // Rule. + for (const rule of credentialAccessBoundary.accessBoundary + .accessBoundaryRules) { if (rule.availablePermissions.length === 0) { throw new Error( 'At least one permission should be defined in access boundary rules.' @@ -111,10 +157,7 @@ export class DownscopedClient extends AuthClient { } this.stsCredential = new sts.StsCredentials(STS_ACCESS_TOKEN_URL); - // Default OAuth scope. This could be overridden via public property. this.cachedDownscopedAccessToken = null; - this.credentialAccessBoundary = cab; - this.authClient = client; // As threshold could be zero, // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the // zero value. @@ -129,39 +172,42 @@ export class DownscopedClient extends AuthClient { /** * Provides a mechanism to inject Downscoped access tokens directly. - * When the provided credential expires, a new credential, using the - * external account options are retrieved. - * Notice DownscopedClient is the broker class mainly used for generate - * downscoped access tokens, it is unlikely we call this function in real - * use case. - * We implement to make this a helper function for testing all cases in getAccessToken(). + * The expiry_date field is required to facilitate determination of the token + * expiration which would make it easier for the token consumer to handle. * @param credentials The Credentials object to set on the current client. */ setCredentials(credentials: Credentials) { + if (!credentials.expiry_date) { + throw new Error( + 'The access token expiry_date field is missing in the provided ' + + 'credentials.' + ); + } super.setCredentials(credentials); this.cachedDownscopedAccessToken = credentials; } - async getAccessToken(): Promise { + async getAccessToken(): Promise { // If the cached access token is unavailable or expired, force refresh. - // The Downscoped access token will be returned in GetAccessTokenResponse format. - // If cached access token is unavailable or expired, force refresh. + // The Downscoped access token will be returned in + // DownscopedAccessTokenResponse format. if ( !this.cachedDownscopedAccessToken || this.isExpired(this.cachedDownscopedAccessToken) ) { await this.refreshAccessTokenAsync(); } - // Return Downscoped access token in GetAccessTokenResponse format. + // Return Downscoped access token in DownscopedAccessTokenResponse format. return { token: this.cachedDownscopedAccessToken!.access_token, + expirationTime: this.cachedDownscopedAccessToken!.expiry_date, res: this.cachedDownscopedAccessToken!.res, }; } /** * The main authentication interface. It takes an optional url which when - * present is the endpoint> being accessed, and returns a Promise which + * present is the endpoint being accessed, and returns a Promise which * resolves with authorization header fields. * * The result has the form: @@ -178,7 +224,7 @@ export class DownscopedClient extends AuthClient { * @param opts Request options. * @param callback callback. * @return A promise that resolves with the HTTP response when no callback - * is provided. + * is provided. */ request(opts: GaxiosOptions): GaxiosPromise; request(opts: GaxiosOptions, callback: BodyResponseCallback): void; @@ -192,13 +238,14 @@ export class DownscopedClient extends AuthClient { /** * Forces token refresh, even if unexpired tokens are currently cached. * GCP access tokens are retrieved from authclient object/source credential. - * Thenm GCP access tokens are exchanged for downscoped access tokens via the + * Then GCP access tokens are exchanged for downscoped access tokens via the * token exchange endpoint. * @return A promise that resolves with the fresh downscoped access token. */ protected async refreshAccessTokenAsync(): Promise { // Retrieve GCP access token from source credential. - const subjectToken = await (await this.authClient.getAccessToken()).token; + const subjectToken = (await this.authClient.getAccessToken()).token; + // Construct the STS credentials options. const stsCredentialsOptions: sts.StsCredentialsOptions = { grantType: STS_GRANT_TYPE, @@ -207,7 +254,8 @@ export class DownscopedClient extends AuthClient { subjectTokenType: STS_SUBJECT_TOKEN_TYPE, }; - // Exchange the source access token for a Downscoped access token. + // Exchange the source AuthClient access token for a Downscoped access + // token. const stsResponse = await this.stsCredential.exchangeToken( stsCredentialsOptions, undefined, diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 65b22d0c..3fe2b2ee 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -122,7 +122,7 @@ export interface StsSuccessfulResponse { token_type: string; expires_in: number; refresh_token?: string; - scope: string; + scope?: string; res?: GaxiosResponse | null; } diff --git a/src/index.ts b/src/index.ts index a0fa19ce..35cf959c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,6 @@ export { } from './auth/credentials'; export {GCPEnv} from './auth/envDetect'; export {GoogleAuthOptions, ProjectIdCallback} from './auth/googleauth'; -export {DownscopedClient} from './auth/downscopedclient'; export {IAMAuth, RequestMetadata} from './auth/iam'; export {IdTokenClient, IdTokenProvider} from './auth/idtokenclient'; export {Claims, JWTAccess} from './auth/jwtaccess'; diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 71aec662..391616e2 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -51,7 +51,6 @@ const poolId = 'POOL_ID'; const providerId = 'PROVIDER_ID'; const baseUrl = 'https://sts.googleapis.com'; const path = '/v1/token'; -const betaPath = '/v1beta/token'; const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; const saBaseUrl = 'https://iamcredentials.googleapis.com'; const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; @@ -76,26 +75,6 @@ export function mockStsTokenExchange( return scope; } -export function mockStsBetaTokenExchange( - nockParams: NockMockStsToken[] -): nock.Scope { - const scope = nock(baseUrl); - nockParams.forEach(nockMockStsToken => { - const headers = Object.assign( - { - 'content-type': 'application/x-www-form-urlencoded', - }, - nockMockStsToken.additionalHeaders || {} - ); - scope - .post(betaPath, qs.stringify(nockMockStsToken.request), { - reqheaders: headers, - }) - .reply(nockMockStsToken.statusCode, nockMockStsToken.response); - }); - return scope; -} - export function mockGenerateAccessToken( nockParams: NockMockGenerateAccessToken[] ): nock.Scope { diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index 06d445b0..b857aac8 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -13,18 +13,51 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it, before, after, beforeEach, afterEach} from 'mocha'; +import {describe, it, beforeEach, afterEach} from 'mocha'; import * as nock from 'nock'; import * as sinon from 'sinon'; +import {GaxiosOptions, GaxiosPromise} from 'gaxios'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; -import {DownscopedClient} from '../src/auth/downscopedclient'; +import { + DownscopedClient, + CredentialAccessBoundary, + MAX_ACCESS_BOUNDARY_RULES_COUNT, +} from '../src/auth/downscopedclient'; import {AuthClient} from '../src/auth/authclient'; -import {mockStsBetaTokenExchange} from './externalclienthelper'; -import {GoogleAuth} from '../src'; +import {mockStsTokenExchange} from './externalclienthelper'; +import { + OAuthErrorResponse, + getErrorFromOAuthErrorResponse, +} from '../src/auth/oauth2common'; +import {GetAccessTokenResponse, Headers} from '../src/auth/oauth2client'; nock.disableNetConnect(); +/** A dummy class used as source credential for testing. */ +class TestAuthClient extends AuthClient { + public throwError = false; + private counter = 0; + + async getAccessToken(): Promise { + if (!this.throwError) { + // Increment subject_token counter each time this is called. + return { + token: `subject_token_${this.counter++}`, + }; + } + throw new Error('Cannot get subject token.'); + } + + async getRequestHeaders(url?: string): Promise { + throw new Error('Not implemented.'); + } + + request(opts: GaxiosOptions): GaxiosPromise { + throw new Error('Not implemented.'); + } +} + interface SampleResponse { foo: string; bar: number; @@ -32,12 +65,7 @@ interface SampleResponse { describe('DownscopedClient', () => { let clock: sinon.SinonFakeTimers; - - const auth = new GoogleAuth({ - keyFilename: './test/fixtures/private.json', - scopes: 'https://www.googleapis.com/auth/cloud-platform', - }); - let client: AuthClient; + let client: TestAuthClient; const ONE_HOUR_IN_SECS = 3600; const testAvailableResource = @@ -67,7 +95,6 @@ describe('DownscopedClient', () => { issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', token_type: 'Bearer', expires_in: ONE_HOUR_IN_SECS, - scope: 'scope1 scope2', }; /** * Offset to take into account network delays and server clock skews. @@ -75,7 +102,7 @@ describe('DownscopedClient', () => { const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000; beforeEach(async () => { - client = await auth.getClient(); + client = new TestAuthClient(); }); afterEach(() => { @@ -100,13 +127,14 @@ describe('DownscopedClient', () => { }, expectedError); }); - it('should throw on exceed number of access boundary rules', () => { + it('should throw when number of access boundary rules is exceeded', () => { const expectedError = new Error( - 'Access boundary rule exceeds limit, max 10 allowed.' + 'The provided access boundary has more than ' + + `${MAX_ACCESS_BOUNDARY_RULES_COUNT} access boundary rules.` ); - const cabWithExceedingAccessBoundaryRules = { + const cabWithExceedingAccessBoundaryRules: CredentialAccessBoundary = { accessBoundary: { - accessBoundaryRules: [] as any, + accessBoundaryRules: [], }, }; const testAccessBoundaryRule = { @@ -116,7 +144,7 @@ describe('DownscopedClient', () => { expression: testAvailabilityConditionExpression, }, }; - for (let num = 0; num <= 10; num++) { + for (let num = 0; num <= MAX_ACCESS_BOUNDARY_RULES_COUNT; num++) { cabWithExceedingAccessBoundaryRules.accessBoundary.accessBoundaryRules.push( testAccessBoundaryRule ); @@ -269,24 +297,50 @@ describe('DownscopedClient', () => { }); }); - describe('getAccessToken()', () => { - let sandbox: sinon.SinonSandbox; - before(() => { - const expectedAccessTokenResponse = { - token: 'subject_token', + describe('setCredential()', () => { + it('should throw error if no expire time is set in credential', async () => { + const credentials = { + access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', }; - sandbox = sinon.createSandbox(); - sandbox - .stub(client, 'getAccessToken') - .resolves(expectedAccessTokenResponse); + const expectedError = new Error( + 'The access token expiry_date field is missing in the provided ' + + 'credentials.' + ); + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + assert.throws(() => { + downscopedClient.setCredentials(credentials); + }, expectedError); }); - after(() => { - sandbox.restore(); + it('should not throw error if expire time is set in credential', async () => { + const now = new Date().getTime(); + const credentials = { + access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', + expiry_date: now + ONE_HOUR_IN_SECS * 1000, + }; + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + assert.doesNotThrow(() => { + downscopedClient.setCredentials(credentials); + }); + const tokenResponse = await downscopedClient.getAccessToken(); + assert.deepStrictEqual(tokenResponse.token, credentials.access_token); + assert.deepStrictEqual( + tokenResponse.expirationTime, + credentials.expiry_date + ); }); + }); + describe('getAccessToken()', () => { it('should return current unexpired cached DownscopedClient access token', async () => { const now = new Date().getTime(); + clock = sinon.useFakeTimers(now); const credentials = { access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', expiry_date: now + ONE_HOUR_IN_SECS * 1000, @@ -298,6 +352,29 @@ describe('DownscopedClient', () => { downscopedClient.setCredentials(credentials); const tokenResponse = await downscopedClient.getAccessToken(); assert.deepStrictEqual(tokenResponse.token, credentials.access_token); + assert.deepStrictEqual( + tokenResponse.expirationTime, + credentials.expiry_date + ); + assert.deepStrictEqual( + tokenResponse.token, + downscopedClient.credentials.access_token + ); + assert.deepStrictEqual( + tokenResponse.expirationTime, + downscopedClient.credentials.expiry_date + ); + + clock.tick(ONE_HOUR_IN_SECS * 1000 - EXPIRATION_TIME_OFFSET - 1); + const cachedTokenResponse = await downscopedClient.getAccessToken(); + assert.deepStrictEqual( + cachedTokenResponse.token, + credentials.access_token + ); + assert.deepStrictEqual( + cachedTokenResponse.expirationTime, + credentials.expiry_date + ); }); it('should refresh a new DownscopedClient access when cached one gets expired', async () => { @@ -307,7 +384,7 @@ describe('DownscopedClient', () => { access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', expiry_date: now + ONE_HOUR_IN_SECS * 1000, }; - const scope = mockStsBetaTokenExchange([ + const scope = mockStsTokenExchange([ { statusCode: 200, response: stsSuccessfulResponse, @@ -315,11 +392,9 @@ describe('DownscopedClient', () => { grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', requested_token_type: 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token', + subject_token: 'subject_token_0', subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', - options: - testClientAccessBoundary && - JSON.stringify(testClientAccessBoundary), + options: JSON.stringify(testClientAccessBoundary), }, }, ]); @@ -336,16 +411,31 @@ describe('DownscopedClient', () => { clock.tick(1); const refreshedTokenResponse = await downscopedClient.getAccessToken(); - + const expectedExpirationTime = + credentials.expiry_date + + stsSuccessfulResponse.expires_in * 1000 - + EXPIRATION_TIME_OFFSET; assert.deepStrictEqual( refreshedTokenResponse.token, stsSuccessfulResponse.access_token ); + assert.deepStrictEqual( + refreshedTokenResponse.expirationTime, + expectedExpirationTime + ); + assert.deepStrictEqual( + refreshedTokenResponse.token, + downscopedClient.credentials.access_token + ); + assert.deepStrictEqual( + refreshedTokenResponse.expirationTime, + downscopedClient.credentials.expiry_date + ); scope.done(); }); - it('should return new DownscopedClient access token when there is no cached downscoped access token', async () => { - const scope = mockStsBetaTokenExchange([ + it('should return new access token when no cached token is available', async () => { + const scope = mockStsTokenExchange([ { statusCode: 200, response: stsSuccessfulResponse, @@ -353,27 +443,103 @@ describe('DownscopedClient', () => { grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', requested_token_type: 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token', + subject_token: 'subject_token_0', subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', - options: - testClientAccessBoundary && - JSON.stringify(testClientAccessBoundary), + options: JSON.stringify(testClientAccessBoundary), }, }, ]); - const downscopedClient = new DownscopedClient( client, testClientAccessBoundary ); + assert.deepStrictEqual(downscopedClient.credentials, {}); const tokenResponse = await downscopedClient.getAccessToken(); - assert.deepStrictEqual( tokenResponse.token, stsSuccessfulResponse.access_token ); + assert.deepStrictEqual( + tokenResponse.token, + downscopedClient.credentials.access_token + ); + assert.deepStrictEqual( + tokenResponse.expirationTime, + downscopedClient.credentials.expiry_date + ); + scope.done(); + }); + + it('should handle underlying token exchange errors', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + const scope = mockStsTokenExchange([ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + assert.deepStrictEqual(downscopedClient.credentials, {}); + await assert.rejects( + downscopedClient.getAccessToken(), + getErrorFromOAuthErrorResponse(errorResponse) + ); + assert.deepStrictEqual(downscopedClient.credentials, {}); + // Next try should succeed. + const actualResponse = await downscopedClient.getAccessToken(); + delete actualResponse.res; + assert.deepStrictEqual( + actualResponse.token, + stsSuccessfulResponse.access_token + ); + assert.deepStrictEqual( + actualResponse.token, + downscopedClient.credentials.access_token + ); + assert.deepStrictEqual( + actualResponse.expirationTime, + downscopedClient.credentials.expiry_date + ); scope.done(); }); + + it('should throw when the source AuthClient rejects on token request', async () => { + const expectedError = new Error('Cannot get subject token.'); + client.throwError = true; + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + await assert.rejects(downscopedClient.getAccessToken(), expectedError); + }); }); describe('getRequestHeader()', () => { diff --git a/test/test.index.ts b/test/test.index.ts index 822eda1c..3d9b0457 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -42,7 +42,6 @@ describe('index', () => { assert(gal.IdentityPoolClient); assert(gal.AwsClient); assert(gal.BaseExternalAccountClient); - assert(gal.DownscopedClient); assert(gal.Impersonated); }); }); From ddf5142f01fe8c58d378ad244cb2320e7bc13527 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 29 Jul 2021 15:40:13 +0000 Subject: [PATCH 270/662] chore: release 7.4.1 (#1222) :robot: I have created a release \*beep\* \*boop\* --- ### [7.4.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.4.0...v7.4.1) (2021-07-29) ### Bug Fixes * **downscoped-client:** bug fixes for downscoped client implementation. ([#1219](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1219)) ([4fbe67e](https://www.github.com/googleapis/google-auth-library-nodejs/commit/4fbe67e08bce3f31193d3bb7b93c4cc1251e66a2)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cff66d86..7d5da2f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.4.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.4.0...v7.4.1) (2021-07-29) + + +### Bug Fixes + +* **downscoped-client:** bug fixes for downscoped client implementation. ([#1219](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1219)) ([4fbe67e](https://www.github.com/googleapis/google-auth-library-nodejs/commit/4fbe67e08bce3f31193d3bb7b93c4cc1251e66a2)) + ## [7.4.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.3.0...v7.4.0) (2021-07-29) diff --git a/package.json b/package.json index a2e6400e..d78f36c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.4.0", + "version": "7.4.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index aa5f5bb6..d4b722e9 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.4.0", + "google-auth-library": "^7.4.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From c6345ff098dcf80505fdaf51746538b80ed56247 Mon Sep 17 00:00:00 2001 From: Averi Kitsch Date: Thu, 29 Jul 2021 11:29:36 -0700 Subject: [PATCH 271/662] sample: update region tag (#1223) --- samples/idtokens-serverless.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index 1bcc85e0..9b02821a 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -23,6 +23,7 @@ function main( targetAudience = null ) { // [START google_auth_idtoken_serverless] + // [START cloudrun_service_to_service_auth] // [START run_service_to_service_auth] // [START functions_bearer_token] /** @@ -51,6 +52,7 @@ function main( }); // [END functions_bearer_token] // [END run_service_to_service_auth] + // [END cloudrun_service_to_service_auth] // [END google_auth_idtoken_serverless] } From c3c7e3fbc300d57a874fb3e2355971feb98fc4b5 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 30 Jul 2021 17:08:08 +0000 Subject: [PATCH 272/662] build: update auto-approve config for new validation (#1169) (#1224) Co-authored-by: Anthonios Partheniou Source-Link: https://github.com/googleapis/synthtool/commit/df7fc1e3a6df4316920ab221431945cdf9aa7217 Post-Processor: gcr.io/repo-automation-bots/owlbot-nodejs:latest@sha256:6245a5be4c0406d9b2f04f380d8b88ffe4655df3cdbb57626f8913e8d620f4dd --- .github/.OwlBot.lock.yaml | 2 +- .github/auto-approve.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index b1434427..9b2b9550 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - digest: sha256:06c970a44680229c1e8cefa701dbc93b80468ec4a34e6968475084e4ec1e2d7d + digest: sha256:6245a5be4c0406d9b2f04f380d8b88ffe4655df3cdbb57626f8913e8d620f4dd diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml index a79ba66c..49cf9422 100644 --- a/.github/auto-approve.yml +++ b/.github/auto-approve.yml @@ -6,7 +6,7 @@ rules: - "CHANGELOG\\.md$" maxFiles: 3 - author: "renovate-bot" - title: "^(fix\\(deps\\)|chore\\(deps\\)):" + title: "^(fix|chore)\\(deps\\):" changedFiles: - - "/package\\.json$" + - "package\\.json$" maxFiles: 2 From 49b70bf61a52d5ab857479bb08ff7c314b28a6f7 Mon Sep 17 00:00:00 2001 From: "F. Hinkelmann" Date: Wed, 4 Aug 2021 16:04:31 -0400 Subject: [PATCH 273/662] chore(nodejs): update client ref docs link in metadata (#1225) --- .repo-metadata.json | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.repo-metadata.json b/.repo-metadata.json index a99de37d..106512f4 100644 --- a/.repo-metadata.json +++ b/.repo-metadata.json @@ -2,7 +2,7 @@ "name": "google-auth-library", "name_pretty": "Google Auth Library", "product_documentation": "https://cloud.google.com/docs/authentication/", - "client_documentation": "https://googleapis.dev/nodejs/google-auth-library/latest", + "client_documentation": "https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest", "release_level": "ga", "language": "nodejs", "repo": "googleapis/google-auth-library-nodejs", diff --git a/README.md b/README.md index 19a4de8b..9c0c633c 100644 --- a/README.md +++ b/README.md @@ -804,7 +804,7 @@ Apache Version 2.0 See [LICENSE](https://github.com/googleapis/google-auth-library-nodejs/blob/master/LICENSE) -[client-docs]: https://googleapis.dev/nodejs/google-auth-library/latest +[client-docs]: https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest [product-docs]: https://cloud.google.com/docs/authentication/ [shell_img]: https://gstatic.com/cloudssh/images/open-btn.png [projects]: https://console.cloud.google.com/project From 24bb4568820c2692b1b3ff29835a38fdb3f28c9e Mon Sep 17 00:00:00 2001 From: Xin Li Date: Wed, 4 Aug 2021 14:39:46 -0700 Subject: [PATCH 274/662] feat: Adds support for STS response not returning expires_in field. (#1216) --- src/auth/baseexternalclient.ts | 8 +- src/auth/downscopedclient.ts | 14 +++- src/auth/stscredentials.ts | 2 +- test/test.baseexternalclient.ts | 55 +++++++++++++ test/test.downscopedclient.ts | 142 +++++++++++++++++++++++++++++++- 5 files changed, 217 insertions(+), 4 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index af93a74d..ca2ca7e6 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -377,13 +377,19 @@ export abstract class BaseExternalAccountClient extends AuthClient { this.cachedAccessToken = await this.getImpersonatedAccessToken( stsResponse.access_token ); - } else { + } else if (stsResponse.expires_in) { // Save response in cached access token. this.cachedAccessToken = { access_token: stsResponse.access_token, expiry_date: new Date().getTime() + stsResponse.expires_in * 1000, res: stsResponse.res, }; + } else { + // Save response in cached access token. + this.cachedAccessToken = { + access_token: stsResponse.access_token, + res: stsResponse.res, + }; } // Save credentials. diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index bbd3d147..0c668029 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -262,10 +262,22 @@ export class DownscopedClient extends AuthClient { this.credentialAccessBoundary ); + /** + * The STS endpoint will only return the expiration time for the downscoped + * access token if the original access token represents a service account. + * The downscoped token's expiration time will always match the source + * credential expiration. When no expires_in is returned, we can copy the + * source credential's expiration time. + */ + const sourceCredExpireDate = + this.authClient.credentials?.expiry_date || null; + const expiryDate = stsResponse.expires_in + ? new Date().getTime() + stsResponse.expires_in * 1000 + : sourceCredExpireDate; // Save response in cached access token. this.cachedDownscopedAccessToken = { access_token: stsResponse.access_token, - expiry_date: new Date().getTime() + stsResponse.expires_in * 1000, + expiry_date: expiryDate, res: stsResponse.res, }; diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 3fe2b2ee..497b1e76 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -120,7 +120,7 @@ export interface StsSuccessfulResponse { access_token: string; issued_token_type: string; token_type: string; - expires_in: number; + expires_in?: number; refresh_token?: string; scope?: string; res?: GaxiosResponse | null; diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index a5ede057..60599031 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -333,6 +333,61 @@ describe('BaseExternalAccountClient', () => { scope.done(); }); + it('should return credential with no expiry date if STS response does not return one', async () => { + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + const emittedEvents: Credentials[] = []; + delete stsSuccessfulResponse2.expires_in; + + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithCreds + ); + // Listen to tokens events. On every event, push to list of + // emittedEvents. + client.on('tokens', tokens => { + emittedEvents.push(tokens); + }); + const actualResponse = await client.getAccessToken(); + + // tokens event should be triggered once with expected event. + assert.strictEqual(emittedEvents.length, 1); + assert.deepStrictEqual(emittedEvents[0], { + refresh_token: null, + expiry_date: undefined, + access_token: stsSuccessfulResponse.access_token, + token_type: 'Bearer', + id_token: null, + }); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse2.access_token, + }); + assert.deepStrictEqual(client.credentials.expiry_date, undefined); + assert.deepStrictEqual( + client.credentials.access_token, + stsSuccessfulResponse2.access_token + ); + scope.done(); + }); + it('should handle underlying token exchange errors', async () => { const errorResponse: OAuthErrorResponse = { error: 'invalid_request', diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index b857aac8..c41a0945 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -18,6 +18,7 @@ import * as nock from 'nock'; import * as sinon from 'sinon'; import {GaxiosOptions, GaxiosPromise} from 'gaxios'; +import {Credentials} from '../src/auth/credentials'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; import { DownscopedClient, @@ -49,6 +50,10 @@ class TestAuthClient extends AuthClient { throw new Error('Cannot get subject token.'); } + set expirationTime(expirationTime: number | undefined | null) { + this.credentials.expiry_date = expirationTime; + } + async getRequestHeaders(url?: string): Promise { throw new Error('Not implemented.'); } @@ -380,6 +385,7 @@ describe('DownscopedClient', () => { it('should refresh a new DownscopedClient access when cached one gets expired', async () => { const now = new Date().getTime(); clock = sinon.useFakeTimers(now); + const emittedEvents: Credentials[] = []; const credentials = { access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', expiry_date: now + ONE_HOUR_IN_SECS * 1000, @@ -403,18 +409,40 @@ describe('DownscopedClient', () => { client, testClientAccessBoundary ); + // Listen to tokens events. On every event, push to list of + // emittedEvents. + downscopedClient.on('tokens', tokens => { + emittedEvents.push(tokens); + }); downscopedClient.setCredentials(credentials); clock.tick(ONE_HOUR_IN_SECS * 1000 - EXPIRATION_TIME_OFFSET - 1); const tokenResponse = await downscopedClient.getAccessToken(); + + // No new event should be triggered since the cached access token is + // returned. + assert.strictEqual(emittedEvents.length, 0); assert.deepStrictEqual(tokenResponse.token, credentials.access_token); clock.tick(1); const refreshedTokenResponse = await downscopedClient.getAccessToken(); + + const responseExpiresIn = stsSuccessfulResponse.expires_in as number; const expectedExpirationTime = credentials.expiry_date + - stsSuccessfulResponse.expires_in * 1000 - + responseExpiresIn * 1000 - EXPIRATION_TIME_OFFSET; + + // tokens event should be triggered once with expected event. + assert.strictEqual(emittedEvents.length, 1); + assert.deepStrictEqual(emittedEvents[0], { + refresh_token: null, + expiry_date: expectedExpirationTime, + access_token: stsSuccessfulResponse.access_token, + token_type: 'Bearer', + id_token: null, + }); + assert.deepStrictEqual( refreshedTokenResponse.token, stsSuccessfulResponse.access_token @@ -534,12 +562,124 @@ describe('DownscopedClient', () => { it('should throw when the source AuthClient rejects on token request', async () => { const expectedError = new Error('Cannot get subject token.'); client.throwError = true; + const downscopedClient = new DownscopedClient( client, testClientAccessBoundary ); await assert.rejects(downscopedClient.getAccessToken(), expectedError); }); + + it('should copy source cred expiry time if STS response does not return expiry time', async () => { + const now = new Date().getTime(); + const expireDate = now + ONE_HOUR_IN_SECS * 1000; + const stsSuccessfulResponseWithoutExpireInField = Object.assign( + {}, + stsSuccessfulResponse + ); + const emittedEvents: Credentials[] = []; + delete stsSuccessfulResponseWithoutExpireInField.expires_in; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponseWithoutExpireInField, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + + client.expirationTime = expireDate; + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + // Listen to tokens events. On every event, push to list of + // emittedEvents. + downscopedClient.on('tokens', tokens => { + emittedEvents.push(tokens); + }); + + const tokenResponse = await downscopedClient.getAccessToken(); + + // tokens event should be triggered once with expected event. + assert.strictEqual(emittedEvents.length, 1); + assert.deepStrictEqual(emittedEvents[0], { + refresh_token: null, + expiry_date: expireDate, + access_token: stsSuccessfulResponseWithoutExpireInField.access_token, + token_type: 'Bearer', + id_token: null, + }); + assert.deepStrictEqual(tokenResponse.expirationTime, expireDate); + assert.deepStrictEqual( + tokenResponse.token, + stsSuccessfulResponseWithoutExpireInField.access_token + ); + assert.strictEqual(downscopedClient.credentials.expiry_date, expireDate); + assert.strictEqual( + downscopedClient.credentials.access_token, + stsSuccessfulResponseWithoutExpireInField.access_token + ); + scope.done(); + }); + + it('should have no expiry date if source cred has no expiry time and STS response does not return one', async () => { + const stsSuccessfulResponseWithoutExpireInField = Object.assign( + {}, + stsSuccessfulResponse + ); + const emittedEvents: Credentials[] = []; + delete stsSuccessfulResponseWithoutExpireInField.expires_in; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponseWithoutExpireInField, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + + const downscopedClient = new DownscopedClient( + client, + testClientAccessBoundary + ); + // Listen to tokens events. On every event, push to list of + // emittedEvents. + downscopedClient.on('tokens', tokens => { + emittedEvents.push(tokens); + }); + + const tokenResponse = await downscopedClient.getAccessToken(); + + // tokens event should be triggered once with expected event. + assert.strictEqual(emittedEvents.length, 1); + assert.deepStrictEqual(emittedEvents[0], { + refresh_token: null, + expiry_date: null, + access_token: stsSuccessfulResponseWithoutExpireInField.access_token, + token_type: 'Bearer', + id_token: null, + }); + assert.deepStrictEqual( + tokenResponse.token, + stsSuccessfulResponseWithoutExpireInField.access_token + ); + assert.deepStrictEqual(tokenResponse.expirationTime, null); + assert.deepStrictEqual(downscopedClient.credentials.expiry_date, null); + scope.done(); + }); }); describe('getRequestHeader()', () => { From 67992392aa4d51be6bf20a2f95f35b7bb97ebaec Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 4 Aug 2021 21:46:29 +0000 Subject: [PATCH 275/662] chore: release 7.5.0 (#1226) :robot: I have created a release \*beep\* \*boop\* --- ## [7.5.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.4.1...v7.5.0) (2021-08-04) ### Features * Adds support for STS response not returning expires_in field. ([#1216](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1216)) ([24bb456](https://www.github.com/googleapis/google-auth-library-nodejs/commit/24bb4568820c2692b1b3ff29835a38fdb3f28c9e)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5da2f6..8d7de753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.5.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.4.1...v7.5.0) (2021-08-04) + + +### Features + +* Adds support for STS response not returning expires_in field. ([#1216](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1216)) ([24bb456](https://www.github.com/googleapis/google-auth-library-nodejs/commit/24bb4568820c2692b1b3ff29835a38fdb3f28c9e)) + ### [7.4.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.4.0...v7.4.1) (2021-07-29) diff --git a/package.json b/package.json index d78f36c7..95324020 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.4.1", + "version": "7.5.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index d4b722e9..e94f4d8b 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.4.1", + "google-auth-library": "^7.5.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 1ca3b733427d951ed624e1129fca510d84d5d0fe Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Tue, 10 Aug 2021 13:36:19 -0700 Subject: [PATCH 276/662] feat: add GoogleAuth.sign() support to external account client (#1227) * feat: add GoogleAuth.sign() support to external account client External account credentials previously did not support signing blobs. The implementation previously depended on service account keys or the service account email in order to call IAMCredentials signBlob. When service account impersonation is used with external account credentials, we can get the impersonated service account email and call the signBlob API with the generated access token, provided the token has the `iam.serviceAccounts.signBlob` permission. This is included in the "Service Account Token Creator" role. Fixes https://github.com/googleapis/google-auth-library-nodejs/issues/1215 --- samples/scripts/externalclient-setup.js | 3 +- samples/test/externalclient.test.js | 38 +++++++++- src/auth/baseexternalclient.ts | 12 ++++ src/auth/googleauth.ts | 31 +++++++- test/externalclienthelper.ts | 2 +- test/test.baseexternalclient.ts | 42 +++++++++++ test/test.googleauth.ts | 94 +++++++++++++++++++++---- 7 files changed, 202 insertions(+), 20 deletions(-) diff --git a/samples/scripts/externalclient-setup.js b/samples/scripts/externalclient-setup.js index a3db1f65..21e75954 100755 --- a/samples/scripts/externalclient-setup.js +++ b/samples/scripts/externalclient-setup.js @@ -29,7 +29,8 @@ // identity pools). // 2. Security Admin (needed to get and set IAM policies). // 3. Service Account Token Creator (needed to generate Google ID tokens and -// access tokens). +// access tokens). This is also needed to call the IAMCredentials signBlob +// API. // // The following APIs need to be enabled on the project: // 1. Identity and Access Management (IAM) API. diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js index 05f699ef..5930fdb2 100644 --- a/samples/test/externalclient.test.js +++ b/samples/test/externalclient.test.js @@ -262,7 +262,7 @@ describe('samples for external-account', () => { type: 'external_account', audience: AUDIENCE_OIDC, subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', - token_url: 'https://sts.googleapis.com/v1beta/token', + token_url: 'https://sts.googleapis.com/v1/token', service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/' + `-/serviceAccounts/${clientEmail}:generateAccessToken`, @@ -285,6 +285,38 @@ describe('samples for external-account', () => { assert.match(output, /DNS Info:/); }); + it('should sign the blobs with IAM credentials API', async () => { + // Create file-sourced configuration JSON file. + // The created OIDC token will be used as the subject token and will be + // retrieved from a file location. + const config = { + type: 'external_account', + audience: AUDIENCE_OIDC, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: 'https://sts.googleapis.com/v1/token', + service_account_impersonation_url: + 'https://iamcredentials.googleapis.com/v1/projects/' + + `-/serviceAccounts/${clientEmail}:generateAccessToken`, + credential_source: { + file: oidcTokenFilePath, + }, + }; + await writeFile(oidcTokenFilePath, oidcToken); + await writeFile(configFilePath, JSON.stringify(config)); + + // Run sample script with GOOGLE_APPLICATION_CREDENTIALS envvar + // pointing to the temporarily created configuration file. + // This script will use signBlob to sign some data using + // service account impersonated workload identity pool credentials. + const output = await execAsync(`${process.execPath} signBlob`, { + env: { + ...process.env, + GOOGLE_APPLICATION_CREDENTIALS: configFilePath, + }, + }); + assert.ok(output.length > 0); + }); + it('should acquire ADC for url-sourced creds', async () => { // Create url-sourced configuration JSON file. // The created OIDC token will be used as the subject token and will be @@ -293,7 +325,7 @@ describe('samples for external-account', () => { type: 'external_account', audience: AUDIENCE_OIDC, subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', - token_url: 'https://sts.googleapis.com/v1beta/token', + token_url: 'https://sts.googleapis.com/v1/token', service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/' + `-/serviceAccounts/${clientEmail}:generateAccessToken`, @@ -358,7 +390,7 @@ describe('samples for external-account', () => { type: 'external_account', audience: AUDIENCE_AWS, subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', - token_url: 'https://sts.googleapis.com/v1beta/token', + token_url: 'https://sts.googleapis.com/v1/token', service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/' + `-/serviceAccounts/${clientEmail}:generateAccessToken`, diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index ca2ca7e6..97db8c64 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -177,6 +177,18 @@ export abstract class BaseExternalAccountClient extends AuthClient { this.projectNumber = this.getProjectNumber(this.audience); } + /** The service account email to be impersonated, if available. */ + getServiceAccountEmail(): string | null { + if (this.serviceAccountImpersonationUrl) { + // Parse email from URL. The formal looks as follows: + // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken + const re = /serviceAccounts\/(?[^:]+):generateAccessToken$/; + const result = re.exec(this.serviceAccountImpersonationUrl); + return result?.groups?.email || null; + } + return null; + } + /** * Provides a mechanism to inject GCP access tokens directly. * When the provided credential expires, a new credential, using the diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 1b037173..6675b7ba 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -20,7 +20,7 @@ import * as os from 'os'; import * as path from 'path'; import * as stream from 'stream'; -import {createCrypto} from '../crypto/crypto'; +import {Crypto, createCrypto} from '../crypto/crypto'; import {DefaultTransporter, Transporter} from '../transporters'; import {Compute, ComputeOptions} from './computeclient'; @@ -877,6 +877,23 @@ export class GoogleAuth { 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 BaseExternalAccountClient && + client.getServiceAccountEmail() + ) { + return this.signBlob( + crypto, + client.getServiceAccountEmail() as string, + data + ); + } + const projectId = await this.getProjectId(); if (!projectId) { throw new Error('Cannot sign data without a project ID.'); @@ -887,7 +904,17 @@ export class GoogleAuth { throw new Error('Cannot sign data without `client_email`.'); } - const url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${creds.client_email}:signBlob`; + return this.signBlob(crypto, creds.client_email, data); + } + + private async signBlob( + crypto: Crypto, + emailOrUniqueId: string, + data: string + ): Promise { + const url = + 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' + + `${emailOrUniqueId}:signBlob`; const res = await this.request({ method: 'POST', url, diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 391616e2..9ab9d4d1 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -51,7 +51,7 @@ const poolId = 'POOL_ID'; const providerId = 'PROVIDER_ID'; const baseUrl = 'https://sts.googleapis.com'; const path = '/v1/token'; -const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; +export const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; const saBaseUrl = 'https://iamcredentials.googleapis.com'; const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 60599031..e6baf436 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -22,6 +22,7 @@ import {StsSuccessfulResponse} from '../src/auth/stscredentials'; import { EXPIRATION_TIME_OFFSET, BaseExternalAccountClient, + BaseExternalAccountClientOptions, } from '../src/auth/baseexternalclient'; import { OAuthErrorResponse, @@ -190,6 +191,47 @@ describe('BaseExternalAccountClient', () => { }); }); + describe('getServiceAccountEmail()', () => { + it('should return the service account email when impersonation is used', () => { + const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; + const saBaseUrl = 'https://iamcredentials.googleapis.com'; + const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; + const options: BaseExternalAccountClientOptions = Object.assign( + {}, + externalAccountOptions + ); + options.service_account_impersonation_url = `${saBaseUrl}${saPath}`; + const client = new TestExternalAccountClient(options); + + assert.strictEqual(client.getServiceAccountEmail(), saEmail); + }); + + it('should return null when impersonation is not used', () => { + const options: BaseExternalAccountClientOptions = Object.assign( + {}, + externalAccountOptions + ); + delete options.service_account_impersonation_url; + const client = new TestExternalAccountClient(options); + + assert(client.getServiceAccountEmail() === null); + }); + + it('should return null when impersonation url is malformed', () => { + const saBaseUrl = 'https://iamcredentials.googleapis.com'; + // Malformed path (missing the service account email). + const saPath = '/v1/projects/-/serviceAccounts/:generateAccessToken'; + const options: BaseExternalAccountClientOptions = Object.assign( + {}, + externalAccountOptions + ); + options.service_account_impersonation_url = `${saBaseUrl}${saPath}`; + const client = new TestExternalAccountClient(options); + + assert(client.getServiceAccountEmail() === null); + }); + }); + describe('getProjectId()', () => { it('should resolve with projectId when determinable', async () => { const projectNumber = 'my-proj-number'; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index a08a98af..f601c25a 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -44,8 +44,11 @@ import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; import {Compute} from '../src/auth/computeclient'; import { + getServiceAccountImpersonationUrl, mockCloudResourceManager, + mockGenerateAccessToken, mockStsTokenExchange, + saEmail, } from './externalclienthelper'; import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; import {AuthClient} from '../src/auth/authclient'; @@ -1596,6 +1599,11 @@ describe('googleauth', () => { expires_in: 3600, scope: 'scope1 scope2', }; + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + 3600 * 1000).toISOString(), + }; const fileSubjectToken = fs.readFileSync( externalAccountJSON.credential_source.file, 'utf-8' @@ -1640,13 +1648,19 @@ describe('googleauth', () => { * manager. * @param mockProjectIdRetrieval Whether to mock project ID retrieval. * @param expectedScopes The list of expected scopes. + * @param mockServiceAccountImpersonation Whether to mock IAMCredentials + * GenerateAccessToken. * @return The list of nock.Scope corresponding to the mocked HTTP * requests. */ function mockGetAccessTokenAndProjectId( mockProjectIdRetrieval = true, - expectedScopes = ['https://www.googleapis.com/auth/cloud-platform'] + expectedScopes = ['https://www.googleapis.com/auth/cloud-platform'], + mockServiceAccountImpersonation = false ): nock.Scope[] { + const stsScopes = mockServiceAccountImpersonation + ? 'https://www.googleapis.com/auth/cloud-platform' + : expectedScopes.join(' '); const scopes = [ mockStsTokenExchange([ { @@ -1655,7 +1669,7 @@ describe('googleauth', () => { request: { grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', audience: externalAccountJSON.audience, - scope: expectedScopes.join(' '), + scope: stsScopes, requested_token_type: 'urn:ietf:params:oauth:token-type:access_token', subject_token: fileSubjectToken, @@ -1664,6 +1678,18 @@ describe('googleauth', () => { }, ]), ]; + if (mockServiceAccountImpersonation) { + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: expectedScopes, + }, + ]) + ); + } if (mockProjectIdRetrieval) { scopes.push( @@ -2163,24 +2189,66 @@ describe('googleauth', () => { }); }); - it('getIdTokenClient() should reject', async () => { - const auth = new GoogleAuth({credentials: createExternalAccountJSON()}); + describe('sign()', () => { + it('should reject when no impersonation is used', async () => { + const scopes = mockGetAccessTokenAndProjectId(); + const auth = new GoogleAuth({ + credentials: createExternalAccountJSON(), + }); - await assert.rejects( - auth.getIdTokenClient('a-target-audience'), - /Cannot fetch ID token in this environment/ - ); + await assert.rejects( + auth.sign('abc123'), + /Cannot sign data without `client_email`/ + ); + scopes.forEach(s => s.done()); + }); + + it('should use IAMCredentials endpoint when impersonation is used', async () => { + const scopes = mockGetAccessTokenAndProjectId( + false, + ['https://www.googleapis.com/auth/cloud-platform'], + true + ); + const email = saEmail; + const configWithImpersonation = createExternalAccountJSON(); + configWithImpersonation.service_account_impersonation_url = + getServiceAccountImpersonationUrl(); + const iamUri = 'https://iamcredentials.googleapis.com'; + const iamPath = `/v1/projects/-/serviceAccounts/${email}:signBlob`; + const signedBlob = 'erutangis'; + const data = 'abc123'; + scopes.push( + nock(iamUri) + .post( + iamPath, + { + payload: Buffer.from(data, 'utf-8').toString('base64'), + }, + { + reqheaders: { + Authorization: `Bearer ${saSuccessResponse.accessToken}`, + 'Content-Type': 'application/json', + }, + } + ) + .reply(200, {signedBlob}) + ); + const auth = new GoogleAuth({credentials: configWithImpersonation}); + + const value = await auth.sign(data); + + scopes.forEach(x => x.done()); + assert.strictEqual(value, signedBlob); + }); }); - it('sign() should reject', async () => { - const scopes = mockGetAccessTokenAndProjectId(); + it('getIdTokenClient() should reject', async () => { const auth = new GoogleAuth({credentials: createExternalAccountJSON()}); await assert.rejects( - auth.sign('abc123'), - /Cannot sign data without `client_email`/ + auth.getIdTokenClient('a-target-audience'), + /Cannot fetch ID token in this environment/ ); - scopes.forEach(s => s.done()); }); it('getAccessToken() should get an access token', async () => { From d77617c3cf40b2f201eac9db561a36d89474e6f8 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 13 Aug 2021 09:59:43 -0700 Subject: [PATCH 277/662] chore: release 7.6.0 (#1231) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d7de753..d5dc7944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.6.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.5.0...v7.6.0) (2021-08-10) + + +### Features + +* add GoogleAuth.sign() support to external account client ([#1227](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1227)) ([1ca3b73](https://www.github.com/googleapis/google-auth-library-nodejs/commit/1ca3b733427d951ed624e1129fca510d84d5d0fe)) + ## [7.5.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.4.1...v7.5.0) (2021-08-04) diff --git a/package.json b/package.json index 95324020..4d10ebb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.5.0", + "version": "7.6.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index e94f4d8b..945b46c0 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.5.0", + "google-auth-library": "^7.6.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From ef41fe59b423125c607e3ad20896a35f12f5365b Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Fri, 13 Aug 2021 10:27:09 -0700 Subject: [PATCH 278/662] fix: use updated variable name for self-signed JWTs (#1233) --- CHANGELOG.md | 2 +- src/auth/googleauth.ts | 4 ++-- src/auth/jwtclient.ts | 2 +- test/test.googleauth.ts | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5dc7944..56c0f6cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,7 @@ ### Features -* add useJWTAccessAlways and defaultServicePath variable ([#1204](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1204)) ([79e100e](https://www.github.com/googleapis/google-auth-library-nodejs/commit/79e100e9ddc64f34e34d0e91c8188f1818e33a1c)) +* add useJWTAccessWithScope and defaultServicePath variable ([#1204](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1204)) ([79e100e](https://www.github.com/googleapis/google-auth-library-nodejs/commit/79e100e9ddc64f34e34d0e91c8188f1818e33a1c)) ## [7.2.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.1.2...v7.2.0) (2021-06-30) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 6675b7ba..b6e2ff0e 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -120,7 +120,7 @@ export class GoogleAuth { * @private */ private checkIsGCE?: boolean = undefined; - useJWTAccessAlways?: boolean; + useJWTAccessWithScope?: boolean; defaultServicePath?: string; // Note: this properly is only public to satisify unit tests. @@ -165,7 +165,7 @@ export class GoogleAuth { // and sign the JWT with the correct audience and scopes (if not supplied). setGapicJWTValues(client: JWT) { client.defaultServicePath = this.defaultServicePath; - client.useJWTAccessAlways = this.useJWTAccessAlways; + client.useJWTAccessWithScope = this.useJWTAccessWithScope; client.defaultScopes = this.defaultScopes; } diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 34483114..937d0b4a 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -46,7 +46,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { subject?: string; gtoken?: GoogleToken; additionalClaims?: {}; - useJWTAccessAlways?: boolean; + useJWTAccessWithScope?: boolean; defaultServicePath?: string; private access?: JWTAccess; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index f601c25a..c396c337 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -410,11 +410,11 @@ describe('googleauth', () => { const result = auth.fromJSON(json); assert.strictEqual(undefined, (result as JWT).scopes); }); - it('fromJSON should set useJWTAccessAlways with private key', () => { - auth.useJWTAccessAlways = true; + it('fromJSON should set useJWTAccessWithScope with private key', () => { + auth.useJWTAccessWithScope = true; const json = createJwtJSON(); const result = auth.fromJSON(json); - assert.ok((result as JWT).useJWTAccessAlways); + assert.ok((result as JWT).useJWTAccessWithScope); }); it('fromJSON should set default service path with private key', () => { From 0dfb42962bf364dccd00205741a4cf387aec619b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 13 Aug 2021 10:59:27 -0700 Subject: [PATCH 279/662] chore: release 7.6.1 (#1234) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56c0f6cb..925897e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.6.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.6.0...v7.6.1) (2021-08-13) + + +### Bug Fixes + +* use updated variable name for self-signed JWTs ([#1233](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1233)) ([ef41fe5](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ef41fe59b423125c607e3ad20896a35f12f5365b)) + ## [7.6.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.5.0...v7.6.0) (2021-08-10) diff --git a/package.json b/package.json index 4d10ebb4..9a6fe462 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.6.0", + "version": "7.6.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 945b46c0..2930192d 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.6.0", + "google-auth-library": "^7.6.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 0360bb722aaa082c36c1e1919bf5df27efbe15b3 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Tue, 17 Aug 2021 12:42:57 -0700 Subject: [PATCH 280/662] fix: validate token_url and service_account_impersonation_url (#1229) --- src/auth/baseexternalclient.ts | 73 +++++++++++++++++++++ test/test.baseexternalclient.ts | 109 ++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 97db8c64..f407ad23 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -37,6 +37,11 @@ const STS_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'; const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; /** The default OAuth scope to request when none is provided. */ const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'; +/** The google apis domain pattern. */ +const GOOGLE_APIS_DOMAIN_PATTERN = '\\.googleapis\\.com$'; +/** The variable portion pattern in a Google APIs domain. */ +const VARIABLE_PORTION_PATTERN = '[^\\.\\s\\/\\\\]+'; + /** * Offset to take into account network delays and server clock skews. */ @@ -154,6 +159,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { clientSecret: options.client_secret, } as ClientAuthentication) : undefined; + if (!this.validateGoogleAPIsUrl('sts', options.token_url)) { + throw new Error(`"${options.token_url}" is not a valid token url.`); + } this.stsCredential = new sts.StsCredentials(options.token_url, clientAuth); // Default OAuth scope. This could be overridden via public property. this.scopes = [DEFAULT_OAUTH_SCOPE]; @@ -161,6 +169,18 @@ export abstract class BaseExternalAccountClient extends AuthClient { this.audience = options.audience; this.subjectTokenType = options.subject_token_type; this.quotaProjectId = options.quota_project_id; + if ( + typeof options.service_account_impersonation_url !== 'undefined' && + !this.validateGoogleAPIsUrl( + 'iamcredentials', + options.service_account_impersonation_url + ) + ) { + throw new Error( + `"${options.service_account_impersonation_url}" is ` + + 'not a valid service account impersonation url.' + ); + } this.serviceAccountImpersonationUrl = options.service_account_impersonation_url; // As threshold could be zero, @@ -501,4 +521,57 @@ export abstract class BaseExternalAccountClient extends AuthClient { return this.scopes; } } + + /** + * Checks whether Google APIs URL is valid. + * @param apiName The apiName of url. + * @param url The Google API URL to validate. + * @return Whether the URL is valid or not. + */ + private validateGoogleAPIsUrl(apiName: string, url: string): boolean { + let parsedUrl; + // Return false if error is thrown during parsing URL. + try { + parsedUrl = new URL(url); + } catch (e) { + return false; + } + + const urlDomain = parsedUrl.hostname; + // Check the protocol is https. + if (parsedUrl.protocol !== 'https:') { + return false; + } + + const googleAPIsDomainPatterns: RegExp[] = [ + new RegExp( + '^' + + VARIABLE_PORTION_PATTERN + + '\\.' + + apiName + + GOOGLE_APIS_DOMAIN_PATTERN + ), + new RegExp('^' + apiName + GOOGLE_APIS_DOMAIN_PATTERN), + new RegExp( + '^' + + apiName + + '\\.' + + VARIABLE_PORTION_PATTERN + + GOOGLE_APIS_DOMAIN_PATTERN + ), + new RegExp( + '^' + + VARIABLE_PORTION_PATTERN + + '\\-' + + apiName + + GOOGLE_APIS_DOMAIN_PATTERN + ), + ]; + for (const googleAPIsDomainPattern of googleAPIsDomainPatterns) { + if (urlDomain.match(googleAPIsDomainPattern)) { + return true; + } + } + return false; + } } diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index e6baf436..9961529a 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -136,6 +136,115 @@ describe('BaseExternalAccountClient', () => { }, expectedError); }); + const invalidTokenUrls = [ + 'http://sts.googleapis.com', + 'https://', + 'https://sts.google.com', + 'https://sts.googleapis.net', + 'https://sts.googleapis.comevil.com', + 'https://sts.googleapis.com.evil.com', + 'https://sts.googleapis.com.evil.com/path/to/example', + 'https://sts..googleapis.com', + 'https://-sts.googleapis.com', + 'https://evilsts.googleapis.com', + 'https://us.east.1.sts.googleapis.com', + 'https://us east 1.sts.googleapis.com', + 'https://us-east- 1.sts.googleapis.com', + 'https://us/.east/.1.sts.googleapis.com', + 'https://us.ea\\st.1.sts.googleapis.com', + ]; + invalidTokenUrls.forEach(invalidTokenUrl => { + it(`should throw on invalid token url: ${invalidTokenUrl}`, () => { + const invalidOptions = Object.assign({}, externalAccountOptions); + invalidOptions.token_url = invalidTokenUrl; + const expectedError = new Error( + `"${invalidTokenUrl}" is not a valid token url.` + ); + assert.throws(() => { + return new TestExternalAccountClient(invalidOptions); + }, expectedError); + }); + }); + + it('should not throw on valid token urls', () => { + const validTokenUrls = [ + 'https://sts.googleapis.com', + 'https://sts.us-west-1.googleapis.com', + 'https://sts.google.googleapis.com', + 'https://sts.googleapis.com/path/to/example', + 'https://us-west-1.sts.googleapis.com', + 'https://us-west-1-sts.googleapis.com', + 'https://exmaple.sts.googleapis.com', + 'https://example-sts.googleapis.com', + ]; + const validOptions = Object.assign({}, externalAccountOptions); + for (const validTokenUrl of validTokenUrls) { + validOptions.token_url = validTokenUrl; + assert.doesNotThrow(() => { + return new TestExternalAccountClient(validOptions); + }); + } + }); + + const invalidServiceAccountImpersonationUrls = [ + 'http://iamcredentials.googleapis.com', + 'https://', + 'https://iamcredentials.google.com', + 'https://iamcredentials.googleapis.net', + 'https://iamcredentials.googleapis.comevil.com', + 'https://iamcredentials.googleapis.com.evil.com', + 'https://iamcredentials.googleapis.com.evil.com/path/to/example', + 'https://iamcredentials..googleapis.com', + 'https://-iamcredentials.googleapis.com', + 'https://eviliamcredentials.googleapis.com', + 'https://evil.eviliamcredentials.googleapis.com', + 'https://us.east.1.iamcredentials.googleapis.com', + 'https://us east 1.iamcredentials.googleapis.com', + 'https://us-east- 1.iamcredentials.googleapis.com', + 'https://us/.east/.1.iamcredentials.googleapis.com', + 'https://us.ea\\st.1.iamcredentials.googleapis.com', + ]; + invalidServiceAccountImpersonationUrls.forEach( + invalidServiceAccountImpersonationUrl => { + it(`should throw on invalid service account impersonation url: ${invalidServiceAccountImpersonationUrl}`, () => { + const invalidOptions = Object.assign( + {}, + externalAccountOptionsWithSA + ); + invalidOptions.service_account_impersonation_url = + invalidServiceAccountImpersonationUrl; + const expectedError = new Error( + `"${invalidServiceAccountImpersonationUrl}" is ` + + 'not a valid service account impersonation url.' + ); + assert.throws(() => { + return new TestExternalAccountClient(invalidOptions); + }, expectedError); + }); + } + ); + + it('should not throw on valid service account impersonation url', () => { + const validServiceAccountImpersonationUrls = [ + 'https://iamcredentials.googleapis.com', + 'https://iamcredentials.us-west-1.googleapis.com', + 'https://iamcredentials.google.googleapis.com', + 'https://iamcredentials.googleapis.com/path/to/example', + 'https://us-west-1.iamcredentials.googleapis.com', + 'https://us-west-1-iamcredentials.googleapis.com', + 'https://example.iamcredentials.googleapis.com', + 'https://example-iamcredentials.googleapis.com', + ]; + const validOptions = Object.assign({}, externalAccountOptionsWithSA); + for (const validServiceAccountImpersonationUrl of validServiceAccountImpersonationUrls) { + validOptions.service_account_impersonation_url = + validServiceAccountImpersonationUrl; + assert.doesNotThrow(() => { + return new TestExternalAccountClient(validOptions); + }); + } + }); + it('should not throw on valid options', () => { assert.doesNotThrow(() => { return new TestExternalAccountClient(externalAccountOptions); From e5ab8e52c1d372c2e8afcc439413e25df55560b8 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 19:50:23 +0000 Subject: [PATCH 281/662] chore: release 7.6.2 (#1235) :robot: I have created a release \*beep\* \*boop\* --- ### [7.6.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.6.1...v7.6.2) (2021-08-17) ### Bug Fixes * validate token_url and service_account_impersonation_url ([#1229](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1229)) ([0360bb7](https://www.github.com/googleapis/google-auth-library-nodejs/commit/0360bb722aaa082c36c1e1919bf5df27efbe15b3)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 925897e2..686a0da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.6.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.6.1...v7.6.2) (2021-08-17) + + +### Bug Fixes + +* validate token_url and service_account_impersonation_url ([#1229](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1229)) ([0360bb7](https://www.github.com/googleapis/google-auth-library-nodejs/commit/0360bb722aaa082c36c1e1919bf5df27efbe15b3)) + ### [7.6.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.6.0...v7.6.1) (2021-08-13) diff --git a/package.json b/package.json index 9a6fe462..4c4bb98b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.6.1", + "version": "7.6.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 2930192d..e6f0a4fb 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.6.1", + "google-auth-library": "^7.6.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 026b67768405887470237fd2ab704004ecef1d40 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 25 Aug 2021 23:34:28 +0000 Subject: [PATCH 282/662] chore: disable renovate dependency dashboard (#1194) (#1237) --- .github/.OwlBot.lock.yaml | 2 +- .kokoro/continuous/node10/common.cfg | 2 +- .kokoro/continuous/node10/test.cfg | 2 +- .kokoro/presubmit/node10/common.cfg | 2 +- .kokoro/samples-test.sh | 2 +- .kokoro/system-test.sh | 2 +- .kokoro/test.sh | 2 +- README.md | 4 ++-- renovate.json | 3 ++- 9 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 9b2b9550..c45b2393 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - digest: sha256:6245a5be4c0406d9b2f04f380d8b88ffe4655df3cdbb57626f8913e8d620f4dd + digest: sha256:667a9e46a9aa5b80240ad164d55ac33bc9d6780b5ef42f125a41f0ad95bc1950 diff --git a/.kokoro/continuous/node10/common.cfg b/.kokoro/continuous/node10/common.cfg index d4231f90..d144aee1 100644 --- a/.kokoro/continuous/node10/common.cfg +++ b/.kokoro/continuous/node10/common.cfg @@ -7,7 +7,7 @@ action { } } -# Bring in codecov.io master token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token +# Bring in codecov.io token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token before_action { fetch_keystore { keystore_resource { diff --git a/.kokoro/continuous/node10/test.cfg b/.kokoro/continuous/node10/test.cfg index 468b8c71..609c0cf0 100644 --- a/.kokoro/continuous/node10/test.cfg +++ b/.kokoro/continuous/node10/test.cfg @@ -1,4 +1,4 @@ -# Bring in codecov.io master token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token +# Bring in codecov.io token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token before_action { fetch_keystore { keystore_resource { diff --git a/.kokoro/presubmit/node10/common.cfg b/.kokoro/presubmit/node10/common.cfg index d4231f90..d144aee1 100644 --- a/.kokoro/presubmit/node10/common.cfg +++ b/.kokoro/presubmit/node10/common.cfg @@ -7,7 +7,7 @@ action { } } -# Bring in codecov.io master token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token +# Bring in codecov.io token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token before_action { fetch_keystore { keystore_resource { diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index 950f8483..f249d3e4 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -39,7 +39,7 @@ if [ -f samples/package.json ]; then npm link ../ npm install cd .. - # If tests are running against master, configure flakybot + # If tests are running against main branch, configure flakybot # to open issues on failures: if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 319d1e0e..0a840452 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -33,7 +33,7 @@ fi npm install -# If tests are running against master, configure flakybot +# If tests are running against main branch, configure flakybot # to open issues on failures: if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml diff --git a/.kokoro/test.sh b/.kokoro/test.sh index b5646aeb..af1ce7e3 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -21,7 +21,7 @@ export NPM_CONFIG_PREFIX=${HOME}/.npm-global cd $(dirname $0)/.. npm install -# If tests are running against master, configure flakybot +# If tests are running against main branch, configure flakybot # to open issues on failures: if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml diff --git a/README.md b/README.md index 9c0c633c..15f9ffab 100644 --- a/README.md +++ b/README.md @@ -795,8 +795,8 @@ Contributions welcome! See the [Contributing Guide](https://github.com/googleapi Please note that this `README.md`, the `samples/README.md`, and a variety of configuration files in this repository (including `.nycrc` and `tsconfig.json`) are generated from a central template. To edit one of these files, make an edit -to its template in this -[directory](https://github.com/googleapis/synthtool/tree/master/synthtool/gcp/templates/node_library). +to its templates in +[directory](https://github.com/googleapis/synthtool). ## License diff --git a/renovate.json b/renovate.json index 9518bf36..26428fcf 100644 --- a/renovate.json +++ b/renovate.json @@ -1,7 +1,8 @@ { "extends": [ "config:base", - "docker:disable" + "docker:disable", + ":disableDependencyDashboard" ], "pinVersions": false, "rebaseStalePrs": true, From 2fcab77a1fae85489829f22ec95cc66b0b284342 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Fri, 27 Aug 2021 14:17:13 -0700 Subject: [PATCH 283/662] feat: add refreshHandler callback to OAuth 2.0 client to handle token refresh (#1213) --- src/auth/oauth2client.ts | 96 ++++++++++++++- test/test.oauth2.ts | 254 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 327 insertions(+), 23 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 84420b6c..9566cc12 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -298,6 +298,15 @@ export interface GenerateAuthUrlOpts { code_challenge?: string; } +export interface AccessTokenResponse { + access_token: string; + expiry_date: number; +} + +export interface GetRefreshHandlerCallback { + (): Promise; +} + export interface GetTokenCallback { ( err: GaxiosError | null, @@ -427,6 +436,8 @@ export class OAuth2Client extends AuthClient { forceRefreshOnFailure: boolean; + refreshHandler?: GetRefreshHandlerCallback; + /** * Handles OAuth2 flow for Google APIs. * @@ -749,7 +760,18 @@ export class OAuth2Client extends AuthClient { !this.credentials.access_token || this.isTokenExpiring(); if (shouldRefresh) { if (!this.credentials.refresh_token) { - throw new Error('No refresh token is set.'); + if (this.refreshHandler) { + const refreshedAccessToken = + await this.processAndValidateRefreshHandler(); + if (refreshedAccessToken?.access_token) { + this.setCredentials(refreshedAccessToken); + return {token: this.credentials.access_token}; + } + } else { + throw new Error( + 'No refresh token or refresh handler callback is set.' + ); + } } const r = await this.refreshAccessTokenAsync(); @@ -781,8 +803,15 @@ export class OAuth2Client extends AuthClient { url?: string | null ): Promise { const thisCreds = this.credentials; - if (!thisCreds.access_token && !thisCreds.refresh_token && !this.apiKey) { - throw new Error('No access, refresh token or API key is set.'); + if ( + !thisCreds.access_token && + !thisCreds.refresh_token && + !this.apiKey && + !this.refreshHandler + ) { + throw new Error( + 'No access, refresh token, API key or refresh handler callback is set.' + ); } if (thisCreds.access_token && !this.isTokenExpiring()) { @@ -793,6 +822,19 @@ export class OAuth2Client extends AuthClient { return {headers: this.addSharedMetadataHeaders(headers)}; } + // If refreshHandler exists, call processAndValidateRefreshHandler(). + if (this.refreshHandler) { + const refreshedAccessToken = + await this.processAndValidateRefreshHandler(); + if (refreshedAccessToken?.access_token) { + this.setCredentials(refreshedAccessToken); + const headers = { + Authorization: 'Bearer ' + this.credentials.access_token, + }; + return {headers: this.addSharedMetadataHeaders(headers)}; + } + } + if (this.apiKey) { return {headers: {'X-Goog-Api-Key': this.apiKey}}; } @@ -945,16 +987,44 @@ export class OAuth2Client extends AuthClient { // fails on the first try because it's expired. Some developers may // choose to enable forceRefreshOnFailure to mitigate time-related // errors. + // Or the following criteria are true: + // - We haven't already retried. It only makes sense to retry once. + // - The response was a 401 or a 403 + // - The request didn't send a readableStream + // - No refresh_token was available + // - An access_token and a refreshHandler callback were available, but + // either no expiry_date was available or the forceRefreshOnFailure + // flag is set. The access_token fails on the first try because it's + // expired. Some developers may choose to enable forceRefreshOnFailure + // to mitigate time-related errors. const mayRequireRefresh = this.credentials && this.credentials.access_token && this.credentials.refresh_token && (!this.credentials.expiry_date || this.forceRefreshOnFailure); + const mayRequireRefreshWithNoRefreshToken = + this.credentials && + this.credentials.access_token && + !this.credentials.refresh_token && + (!this.credentials.expiry_date || this.forceRefreshOnFailure) && + this.refreshHandler; const isReadableStream = res.config.data instanceof stream.Readable; const isAuthErr = statusCode === 401 || statusCode === 403; if (!retry && isAuthErr && !isReadableStream && mayRequireRefresh) { await this.refreshAccessTokenAsync(); return this.requestAsync(opts, true); + } else if ( + !retry && + isAuthErr && + !isReadableStream && + mayRequireRefreshWithNoRefreshToken + ) { + const refreshedAccessToken = + await this.processAndValidateRefreshHandler(); + if (refreshedAccessToken?.access_token) { + this.setCredentials(refreshedAccessToken); + } + return this.requestAsync(opts, true); } } throw e; @@ -1316,6 +1386,26 @@ export class OAuth2Client extends AuthClient { return new LoginTicket(envelope, payload); } + /** + * Returns a promise that resolves with AccessTokenResponse type if + * refreshHandler is defined. + * If not, nothing is returned. + */ + private async processAndValidateRefreshHandler(): Promise< + AccessTokenResponse | undefined + > { + if (this.refreshHandler) { + const accessTokenResponse = await this.refreshHandler(); + if (!accessTokenResponse.access_token) { + throw new Error( + 'No access token is returned by the refreshHandler callback.' + ); + } + return accessTokenResponse; + } + return; + } + /** * Returns true if a token is expired or will expire within * eagerRefreshThresholdMillismilliseconds. diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 62fd416a..ea85ff1e 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -893,7 +893,7 @@ describe('oauth2', () => { client.request({}, (err, result) => { assert.strictEqual( err!.message, - 'No access, refresh token or API key is set.' + 'No access, refresh token, API key or refresh handler callback is set.' ); assert.strictEqual(result, undefined); done(); @@ -1106,19 +1106,30 @@ describe('oauth2', () => { [401, 403].forEach(code => { it(`should refresh token if the server returns ${code}`, done => { - const scope = nock('http://example.com') - .get('/access') - .reply(code, { - error: {code, message: 'Invalid Credentials'}, - }); - const scopes = mockExample(); + const scopes = [ + nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }) + .get('/access', undefined, { + reqheaders: {Authorization: 'Bearer abc123'}, + }) + .reply(200), + nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, {access_token: 'abc123', expires_in: 1000}), + ]; client.credentials = { access_token: 'initial-access-token', refresh_token: 'refresh-token-placeholder', }; - client.request({url: 'http://example.com/access'}, () => { - scope.done(); - scopes[0].done(); + + client.request({url: 'http://example.com/access'}, err => { + assert.strictEqual(err, null); + scopes.forEach(scope => scope.done()); assert.strictEqual('abc123', client.credentials.access_token); done(); }); @@ -1131,24 +1142,109 @@ describe('oauth2', () => { redirectUri: REDIRECT_URI, forceRefreshOnFailure: true, }); - const scope = nock('http://example.com') - .get('/access') - .reply(code, { - error: {code, message: 'Invalid Credentials'}, - }); - const scopes = mockExample(); + const scopes = [ + nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .reply(200, {access_token: 'abc123', expires_in: 1000}), + nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }) + .get('/access', undefined, { + reqheaders: {Authorization: 'Bearer abc123'}, + }) + .reply(200), + ]; client.credentials = { access_token: 'initial-access-token', refresh_token: 'refresh-token-placeholder', expiry_date: new Date().getTime() + 500000, }; - client.request({url: 'http://example.com/access'}, () => { - scope.done(); - scopes[0].done(); + + client.request({url: 'http://example.com/access'}, err => { + assert.strictEqual(err, null); + scopes.forEach(scope => scope.done()); assert.strictEqual('abc123', client.credentials.access_token); done(); }); }); + + it('should call refreshHandler in request() on token expiration and no refresh token available', async () => { + const authHeaders = { + Authorization: 'Bearer access_token', + }; + const scope = nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }) + .get('/access', undefined, { + reqheaders: authHeaders, + }) + .reply(200, {foo: 'bar'}); + const expectedRefreshedAccessToken = { + access_token: 'access_token', + expiry_date: new Date().getTime() + 3600 * 1000, + }; + client.refreshHandler = async () => { + return expectedRefreshedAccessToken; + }; + client.setCredentials({ + access_token: 'initial-access-token', + expiry_date: new Date().getTime() - 1000, + }); + + client.request({url: 'http://example.com/access'}, err => { + assert.strictEqual(err, null); + scope.done(); + assert.strictEqual( + client.credentials.access_token, + expectedRefreshedAccessToken.access_token + ); + assert.strictEqual( + client.credentials.expiry_date, + expectedRefreshedAccessToken.expiry_date + ); + }); + }); + + it('should call refreshHandler in request() if no credentials available', async () => { + const authHeaders = { + Authorization: 'Bearer access_token', + }; + const scope = nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }) + .get('/access', undefined, { + reqheaders: authHeaders, + }) + .reply(200, {foo: 'bar'}); + const expectedRefreshedAccessToken = { + access_token: 'access_token', + expiry_date: new Date().getTime() + 3600 * 1000, + }; + client.refreshHandler = async () => { + return expectedRefreshedAccessToken; + }; + + client.request({url: 'http://example.com/access'}, err => { + assert.strictEqual(err, null); + scope.done(); + assert.strictEqual( + client.credentials.access_token, + expectedRefreshedAccessToken.access_token + ); + assert.strictEqual( + client.credentials.expiry_date, + expectedRefreshedAccessToken.expiry_date + ); + }); + }); }); it('should not retry requests with streaming data', done => { @@ -1338,15 +1434,133 @@ describe('oauth2', () => { assert.deepStrictEqual(info.scopes, tokenInfo.scope.split(' ')); }); - it('should throw if tries to refresh but no refresh token is available', async () => { + it('should call refreshHandler in getRequestHeaders() when no credentials but refreshHandler is available', async () => { + const expectedRefreshedAccessToken = { + access_token: 'access_token', + expiry_date: new Date().getTime() + 3600 * 1000, + }; + client.refreshHandler = async () => { + return expectedRefreshedAccessToken; + }; + const expectedMetadata = { + Authorization: 'Bearer access_token', + }; + assert.deepStrictEqual(client.credentials, {}); + + const requestMetaData = await client.getRequestHeaders( + 'http://example.com' + ); + + assert.deepStrictEqual(requestMetaData, expectedMetadata); + }); + + it('should call refreshHandler in getRequestHeaders() on token expiration and refreshHandler is available', async () => { + const expectedRefreshedAccessToken = { + access_token: 'access_token', + expiry_date: new Date().getTime() + 3600 * 1000, + }; + client.refreshHandler = async () => { + return expectedRefreshedAccessToken; + }; client.setCredentials({ access_token: 'initial-access-token', expiry_date: new Date().getTime() - 1000, }); + const expectedMetadata = { + Authorization: 'Bearer access_token', + }; + + const requestMetaData = await client.getRequestHeaders( + 'http://example.com' + ); + + assert.deepStrictEqual(requestMetaData, expectedMetadata); + }); + + it('should return cached authorization header on getRequestHeaders() if not expired', async () => { + client.credentials = { + access_token: 'initial-access-token', + expiry_date: new Date().getTime() + 3600 * 1000, + }; + const expectedMetadata = { + Authorization: 'Bearer initial-access-token', + }; + + const requestMetaData = await client.getRequestHeaders( + 'http://example.com' + ); + + assert.deepStrictEqual(requestMetaData, expectedMetadata); + }); + + it('should throw on getRequestHeaders() when neither refreshHandler nor refresh token is available', async () => { + client.setCredentials({ + access_token: 'initial-access-token', + expiry_date: new Date().getTime() - 1000, + }); + await assert.rejects( client.getRequestHeaders('http://example.com'), /No refresh token is set./ ); }); + + it('should call refreshHandler in getAccessToken() when neither credentials nor refresh token is available', async () => { + const expectedRefreshedAccessToken = { + access_token: 'access_token', + expiry_date: new Date().getTime() + 3600 * 1000, + }; + client.refreshHandler = async () => { + return expectedRefreshedAccessToken; + }; + assert.deepStrictEqual(client.credentials, {}); + + const refreshedAccessToken = await client.getAccessToken(); + + assert.strictEqual( + refreshedAccessToken.token, + expectedRefreshedAccessToken.access_token + ); + }); + + it('should call refreshHandler in getAccessToken() on expiration and no refresh token available', async () => { + const expectedRefreshedAccessToken = { + access_token: 'access_token', + expiry_date: new Date().getTime() + 3600 * 1000, + }; + client.refreshHandler = async () => { + return expectedRefreshedAccessToken; + }; + client.setCredentials({ + access_token: 'initial-access-token', + expiry_date: new Date().getTime() - 1000, + }); + + const refreshedAccessToken = await client.getAccessToken(); + + assert.strictEqual( + refreshedAccessToken.token, + expectedRefreshedAccessToken.access_token + ); + }); + + it('should throw error if refreshHandler callback response is missing an access token', async () => { + const expectedRefreshedAccessToken = { + access_token: '', + expiry_date: new Date().getTime() + 3600 * 1000, + }; + client.refreshHandler = async () => { + return expectedRefreshedAccessToken; + }; + client.setCredentials({ + access_token: 'initial-access-token', + expiry_date: new Date().getTime() - 1000, + }); + + await assert.rejects( + client.getAccessToken(), + /No access token is returned by the refreshHandler callback./ + ); + }); }); }); From 3d91eae54504becb69dc88cff3a3662dfde2811d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Sun, 29 Aug 2021 12:20:46 -0700 Subject: [PATCH 284/662] chore: release 7.7.0 (#1240) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 686a0da4..03f46c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.7.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.6.2...v7.7.0) (2021-08-27) + + +### Features + +* add refreshHandler callback to OAuth 2.0 client to handle token refresh ([#1213](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1213)) ([2fcab77](https://www.github.com/googleapis/google-auth-library-nodejs/commit/2fcab77a1fae85489829f22ec95cc66b0b284342)) + ### [7.6.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.6.1...v7.6.2) (2021-08-17) diff --git a/package.json b/package.json index 4c4bb98b..8a63c945 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.6.2", + "version": "7.7.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index e6f0a4fb..73d55f0e 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.6.2", + "google-auth-library": "^7.7.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From ad3f652e8e859d8d8a69dfe9df01d001862fe0ae Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Mon, 30 Aug 2021 15:36:54 -0500 Subject: [PATCH 285/662] feat: use self-signed JWTs if alwaysUseJWTAccessWithScope is true (#1196) * feat: use self-signed JWTs if alwaysUseJWTAccessWithScope is true Co-authored-by: Jeffrey Rennie Co-authored-by: Benjamin E. Coe Co-authored-by: Brent Shaffer --- src/auth/jwtaccess.ts | 68 +++++++-- src/auth/jwtclient.ts | 22 ++- test/test.googleauth.ts | 14 ++ test/test.jwt.ts | 325 ++++++++++++++++++++++++++++++++++++++-- test/test.jwtaccess.ts | 46 ++++++ 5 files changed, 445 insertions(+), 30 deletions(-) diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index 30298b6c..88f792dd 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -63,6 +63,28 @@ export class JWTAccess { eagerRefreshThresholdMillis ?? 5 * 60 * 1000; } + /** + * Ensures that we're caching a key appropriately, giving precedence to scopes vs. url + * + * @param url The URI being authorized. + * @param scopes The scope or scopes being authorized + * @returns A string that returns the cached key. + */ + getCachedKey(url?: string, scopes?: string | string[]): string { + let cacheKey = url; + if (scopes && Array.isArray(scopes) && scopes.length) { + cacheKey = url ? `${url}_${scopes.join('_')}` : `${scopes.join('_')}`; + } else if (typeof scopes === 'string') { + cacheKey = url ? `${url}_${scopes}` : scopes; + } + + if (!cacheKey) { + throw Error('Scopes or url must be provided'); + } + + return cacheKey; + } + /** * Get a non-expired access token, after refreshing if necessary. * @@ -71,10 +93,15 @@ export class JWTAccess { * include in the payload. * @returns An object that includes the authorization header. */ - getRequestHeaders(url: string, additionalClaims?: Claims): Headers { + getRequestHeaders( + url?: string, + additionalClaims?: Claims, + scopes?: string | string[] + ): Headers { // Return cached authorization headers, unless we are within // eagerRefreshThresholdMillis ms of them expiring: - const cachedToken = this.cache.get(url); + const key = this.getCachedKey(url, scopes); + const cachedToken = this.cache.get(key); const now = Date.now(); if ( cachedToken && @@ -82,19 +109,34 @@ export class JWTAccess { ) { return cachedToken.headers; } + const iat = Math.floor(Date.now() / 1000); const exp = JWTAccess.getExpirationTime(iat); - // The payload used for signed JWT headers has: - // iss == sub == - // aud == - const defaultClaims = { - iss: this.email, - sub: this.email, - aud: url, - exp, - iat, - }; + let defaultClaims; + // Turn scopes into space-separated string + if (Array.isArray(scopes)) { + scopes = scopes.join(' '); + } + + // If scopes are specified, sign with scopes + if (scopes) { + defaultClaims = { + iss: this.email, + sub: this.email, + scope: scopes, + exp, + iat, + }; + } else { + defaultClaims = { + iss: this.email, + sub: this.email, + aud: url, + exp, + iat, + }; + } // if additionalClaims are provided, ensure they do not collide with // other required claims. @@ -116,7 +158,7 @@ export class JWTAccess { // Sign the jwt and add it to the cache const signedJWT = jws.sign({header, payload, secret: this.key}); const headers = {Authorization: `Bearer ${signedJWT}`}; - this.cache.set(url, { + this.cache.set(key, { expiration: exp * 1000, headers, }); diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 937d0b4a..acacd2b4 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -122,7 +122,11 @@ export class JWT extends OAuth2Client implements IdTokenProvider { protected async getRequestMetadataAsync( url?: string | null ): Promise { - if (!this.apiKey && !this.hasUserScopes() && url) { + url = this.defaultServicePath ? `https://${this.defaultServicePath}/` : url; + const useSelfSignedJWT = + (!this.hasUserScopes() && url) || + (this.useJWTAccessWithScope && this.hasAnyScopes()); + if (!this.apiKey && useSelfSignedJWT) { if ( this.additionalClaims && ( @@ -148,10 +152,22 @@ export class JWT extends OAuth2Client implements IdTokenProvider { this.eagerRefreshThresholdMillis ); } + + let scopes: string | string[] | undefined; + if (this.hasUserScopes()) { + scopes = this.scopes; + } else if (!url) { + scopes = this.defaultScopes; + } + const headers = await this.access.getRequestHeaders( - url, - this.additionalClaims + url ?? undefined, + this.additionalClaims, + // Scopes take precedent over audience for signing, + // so we only provide them if useJWTAccessWithScope is on + this.useJWTAccessWithScope ? scopes : undefined ); + return {headers: this.addSharedMetadataHeaders(headers)}; } } else if (this.hasAnyScopes() || this.apiKey) { diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index c396c337..5266d9f6 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -405,6 +405,20 @@ describe('googleauth', () => { assert.strictEqual(json.private_key, (result as JWT).key); }); + it('fromJSON should set useJWTAccessWithScope with private key', () => { + auth.useJWTAccessWithScope = true; + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.ok((result as JWT).useJWTAccessWithScope); + }); + + it('fromJSON should set default service path with private key', () => { + auth.defaultServicePath = 'a/b/c'; + const json = createJwtJSON(); + const result = auth.fromJSON(json); + assert.strictEqual((result as JWT).defaultServicePath, 'a/b/c'); + }); + it('fromJSON should create JWT with null scopes', () => { const json = createJwtJSON(); const result = auth.fromJSON(json); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index aa70ea44..274fe889 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -197,7 +197,6 @@ describe('jwt', () => { subject: 'ignored@subjectaccount.com', }); jwt.credentials = {refresh_token: 'jwt-placeholder'}; - const testUri = 'http:/example.com/my_test_service'; const got = await jwt.getRequestHeaders(testUri); assert.notStrictEqual(null, got, 'the creds should be present'); @@ -240,12 +239,14 @@ describe('jwt', () => { }); jwt.credentials = {refresh_token: 'jwt-placeholder'}; + jwt.defaultServicePath = 'example.com'; const testUri = 'http:/example.com/my_test_service'; + const testDefault = 'https://example.com/'; const got = await jwt.getRequestHeaders(testUri); assert.notStrictEqual(null, got, 'the creds should be present'); const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); const payload = decoded.payload; - assert.strictEqual(testUri, payload.aud); + assert.strictEqual(testDefault, payload.aud); assert.strictEqual(someClaim, payload.someClaim); }); @@ -790,9 +791,33 @@ describe('jwt', () => { sandbox.restore(); }); - it('uses self signed JWT when no scopes are provided', async () => { + it('returns empty headers if: user scope = false, default scope = false, audience = falsy, useJWTACcessWithScope = false', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual(headers, {}); + }); + + it('returns empty headers if: user scope = false, default scope = false, audience = falsy, useJWTACcessWithScope = truthy', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + jwt.useJWTAccessWithScope = true; + const headers = await jwt.getRequestHeaders(); + assert.deepStrictEqual(headers, {}); + }); + + it('signs JWT with audience if: user scope = false, default scope = false, audience = truthy, useJWTAccessWithScope = false', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ - getRequestHeaders: sinon.stub().returns({}), + getRequestHeaders: stubGetRequestHeaders, }); const jwt = new JWT({ email: 'foo@serviceaccount.com', @@ -800,24 +825,193 @@ describe('jwt', () => { scopes: [], subject: 'bar@subjectaccount.com', }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; + jwt.useJWTAccessWithScope = false; await jwt.getRequestHeaders('https//beepboop.googleapis.com'); sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + undefined + ); }); - it('uses self signed JWT when default scopes are provided', async () => { - const JWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ - getRequestHeaders: sinon.stub().returns({}), + it('signs JWT with audience if: user scope = false, default scope = true, audience = truthy, useJWTAccessWithScope = false', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, }); const jwt = new JWT({ email: 'foo@serviceaccount.com', key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], subject: 'bar@subjectaccount.com', }); - jwt.defaultScopes = ['http://bar', 'http://foo']; - jwt.credentials = {refresh_token: 'jwt-placeholder'}; + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + undefined + ); + }); + + it('signs JWT with audience if: user scope = false, default scope = no, audience = truthy, useJWTAccessWithScope = truthy', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + jwt.useJWTAccessWithScope = true; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + undefined + ); + }); + + it('signs JWT with audience if: user scope = false, default scope = yes, audience = truthy, useJWTAccessWithScope = truthy', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + jwt.useJWTAccessWithScope = true; + jwt.defaultScopes = ['scope1, scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + undefined + ); + }); + + it('signs JWT with audience if: user scope = true, default scope = false, audience = falsy, useJWTAccessWithScope = true', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + }); + jwt.useJWTAccessWithScope = true; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + ['scope1', 'scope2'] + ); + }); + + it('signs JWT with audience if: user scope = false, default scope = true, audience = falsy, useJWTAccessWithScope = true', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: [], + subject: 'bar@subjectaccount.com', + }); + jwt.useJWTAccessWithScope = true; + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders(); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith(stubGetRequestHeaders, undefined, undefined, [ + 'scope1', + 'scope2', + ]); + }); + + it('signs JWT with audience if: user scope = true, default scope = true, audience = falsy, useJWTAccessWithScope = true', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + }); + jwt.useJWTAccessWithScope = true; + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + ['scope1', 'scope2'] + ); + }); + + it('signs JWT with audience if: user scope = true, default scope = false, audience = truthy, useJWTAccessWithScope = true', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + }); + jwt.useJWTAccessWithScope = true; await jwt.getRequestHeaders('https//beepboop.googleapis.com'); - sandbox.assert.calledOnce(JWTAccess); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + ['scope1', 'scope2'] + ); + }); + + it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + }); + jwt.useJWTAccessWithScope = true; + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + ['scope1', 'scope2'] + ); }); it('does not use self signed JWT if target_audience provided', async () => { @@ -881,16 +1075,119 @@ describe('jwt', () => { sandbox.assert.calledTwice(getExpirationTime); sandbox.assert.calledTwice(sign); }); + }); - it('returns no headers when no scopes or audiences are provided', async () => { + describe('oauth2 credentials', () => { + afterEach(() => { + sandbox.restore(); + }); + + it('calls oauth2api if: user scope = false, default scope = true, audience = falsy, useJWTAccessWithScope = false', async () => { const jwt = new JWT({ email: 'foo@serviceaccount.com', - key: fs.readFileSync(PEM_PATH, 'utf8'), + keyFile: PEM_PATH, scopes: [], subject: 'bar@subjectaccount.com', }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + jwt.useJWTAccessWithScope = false; + jwt.defaultScopes = ['scope1', 'scope2']; + const wantedToken = 'abc123'; + const want = `Bearer ${wantedToken}`; + const scope = createGTokenMock({access_token: wantedToken}); const headers = await jwt.getRequestHeaders(); - assert.deepStrictEqual(headers, {}); + scope.done(); + assert.strictEqual( + want, + headers.Authorization, + `the authorization header was wrong: ${headers.Authorization}` + ); + }); + + it('calls oauth2api if: user scope = true, default scope = false, audience = falsy, useJWTAccessWithScope = false', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + const wantedToken = 'abc123'; + const want = `Bearer ${wantedToken}`; + const scope = createGTokenMock({access_token: wantedToken}); + const headers = await jwt.getRequestHeaders(); + scope.done(); + assert.strictEqual( + want, + headers.Authorization, + `the authorization header was wrong: ${headers.Authorization}` + ); + }); + + it('calls oauth2api if: user scope = true, default scope = true, audience = falsy, useJWTAccessWithScope = false', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + jwt.defaultScopes = ['scope1', 'scope2']; + jwt.useJWTAccessWithScope = false; + const wantedToken = 'abc123'; + const want = `Bearer ${wantedToken}`; + const scope = createGTokenMock({access_token: wantedToken}); + const headers = await jwt.getRequestHeaders(); + scope.done(); + assert.strictEqual( + want, + headers.Authorization, + `the authorization header was wrong: ${headers.Authorization}` + ); + }); + + it('calls oauth2api if: user scope = true, default scope = false, audience = truthy, useJWTAccessWithScope = false', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + const wantedToken = 'abc123'; + const testUri = 'http:/example.com/my_test_service'; + const want = `Bearer ${wantedToken}`; + const scope = createGTokenMock({access_token: wantedToken}); + const headers = await jwt.getRequestHeaders(testUri); + scope.done(); + assert.strictEqual( + want, + headers.Authorization, + `the authorization header was wrong: ${headers.Authorization}` + ); + }); + + it('calls oauth2api if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = false', async () => { + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + }); + jwt.credentials = {refresh_token: 'jwt-placeholder'}; + jwt.defaultScopes = ['scope1', 'scope2']; + jwt.useJWTAccessWithScope = false; + const wantedToken = 'abc123'; + const testUri = 'http:/example.com/my_test_service'; + const want = `Bearer ${wantedToken}`; + const scope = createGTokenMock({access_token: wantedToken}); + const headers = await jwt.getRequestHeaders(testUri); + scope.done(); + assert.strictEqual( + want, + headers.Authorization, + `the authorization header was wrong: ${headers.Authorization}` + ); }); }); }); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index 8618faa1..438e84c8 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -56,6 +56,22 @@ describe('jwtaccess', () => { assert.strictEqual(testUri, payload.aud); }); + it('getRequestHeaders should sign with scopes if user supplied scopes', () => { + const client = new JWTAccess(email, keys.private); + const headers = client.getRequestHeaders(testUri, undefined, 'myfakescope'); + const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + const payload = decoded.payload; + assert.strictEqual('myfakescope', payload.scope); + }); + + it('getRequestHeaders should sign with default if user did not supply scopes', () => { + const client = new JWTAccess(email, keys.private); + const headers = client.getRequestHeaders(testUri); + const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + const payload = decoded.payload; + assert.strictEqual(testUri, payload.aud); + }); + it('getRequestHeaders should set key id in header when available', () => { const client = new JWTAccess(email, keys.private, '101'); const headers = client.getRequestHeaders(testUri); @@ -167,4 +183,34 @@ describe('jwtaccess', () => { assert.strictEqual(json.private_key, client.key); assert.strictEqual(json.client_email, client.email); }); + + it('should cache a key with scopes if url & scopes are passed', async () => { + const client = new JWTAccess(email, keys.private); + const testUri = 'http:/example.com/my_test_service'; + const scopes = 'scope1'; + const cacheKey = client.getCachedKey(testUri, scopes); + assert.strictEqual(cacheKey, `${testUri}_${scopes}`); + }); + + it('should cache a key with scopes if url & an array of scopes are passed', async () => { + const client = new JWTAccess(email, keys.private); + const testUri = 'http:/example.com/my_test_service'; + const scopes = ['scope1', 'scope2']; + const cacheKey = client.getCachedKey(testUri, scopes); + assert.strictEqual(cacheKey, `${testUri}_${scopes.join('_')}`); + }); + + it('should cache a key with a URL if nothing else is passed into cacheKey', async () => { + const client = new JWTAccess(email, keys.private); + const testUri = 'http:/example.com/my_test_service'; + const cacheKey = client.getCachedKey(testUri); + assert.strictEqual(cacheKey, testUri); + }); + + it('should throw an error when caching a key if nothing is passed', async () => { + const client = new JWTAccess(email, keys.private); + assert.throws(() => { + client.getCachedKey(); + }, /Scopes or url must be provided/); + }); }); From 98aadf3bda5880e0ecbb8f4ed69b17fe08d09f00 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 20:42:30 +0000 Subject: [PATCH 286/662] chore: release 7.8.0 (#1244) :robot: I have created a release \*beep\* \*boop\* --- ## [7.8.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.7.0...v7.8.0) (2021-08-30) ### Features * use self-signed JWTs if alwaysUseJWTAccessWithScope is true ([#1196](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1196)) ([ad3f652](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ad3f652e8e859d8d8a69dfe9df01d001862fe0ae)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f46c47..d8111f37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.8.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.7.0...v7.8.0) (2021-08-30) + + +### Features + +* use self-signed JWTs if alwaysUseJWTAccessWithScope is true ([#1196](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1196)) ([ad3f652](https://www.github.com/googleapis/google-auth-library-nodejs/commit/ad3f652e8e859d8d8a69dfe9df01d001862fe0ae)) + ## [7.7.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.6.2...v7.7.0) (2021-08-27) diff --git a/package.json b/package.json index 8a63c945..16130770 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.7.0", + "version": "7.8.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 73d55f0e..f02b70d2 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.7.0", + "google-auth-library": "^7.8.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From baa13185836bb299b769c034bfb7d37231eea8d1 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Thu, 2 Sep 2021 14:05:38 -0700 Subject: [PATCH 287/662] feat: wire up implementation of DownscopedClient. (#1232) --- src/auth/downscopedclient.ts | 80 ++++- src/index.ts | 4 + test/test.downscopedclient.ts | 577 +++++++++++++++++++++++++++++++++- test/test.index.ts | 1 + 4 files changed, 648 insertions(+), 14 deletions(-) diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 0c668029..59305d27 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -12,7 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; +import { + GaxiosError, + GaxiosOptions, + GaxiosPromise, + GaxiosResponse, +} from 'gaxios'; +import * as stream from 'stream'; import {BodyResponseCallback} from '../transporters'; import {Credentials} from './credentials'; @@ -122,11 +128,14 @@ export class DownscopedClient extends AuthClient { * @param additionalOptions Optional additional behavior customization * options. These currently customize expiration threshold time and * whether to retry on 401/403 API request errors. + * @param quotaProjectId Optional quota project id for setting up in the + * x-goog-user-project header. */ constructor( private readonly authClient: AuthClient, private readonly credentialAccessBoundary: CredentialAccessBoundary, - additionalOptions?: RefreshOptions + additionalOptions?: RefreshOptions, + quotaProjectId?: string ) { super(); // Check 1-10 Access Boundary Rules are defined within Credential Access @@ -168,6 +177,7 @@ export class DownscopedClient extends AuthClient { .eagerRefreshThresholdMillis as number; } this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; + this.quotaProjectId = quotaProjectId; } /** @@ -214,7 +224,11 @@ export class DownscopedClient extends AuthClient { * { Authorization: 'Bearer ' } */ async getRequestHeaders(): Promise { - throw new Error('Not implemented.'); + const accessTokenResponse = await this.getAccessToken(); + const headers: Headers = { + Authorization: `Bearer ${accessTokenResponse.token}`, + }; + return this.addSharedMetadataHeaders(headers); } /** @@ -232,7 +246,65 @@ export class DownscopedClient extends AuthClient { opts: GaxiosOptions, callback?: BodyResponseCallback ): GaxiosPromise | void { - throw new Error('Not implemented.'); + if (callback) { + this.requestAsync(opts).then( + r => callback(null, r), + e => { + return callback(e, e.response); + } + ); + } else { + return this.requestAsync(opts); + } + } + + /** + * Authenticates the provided HTTP request, processes it and resolves with the + * returned response. + * @param opts The HTTP request options. + * @param retry Whether the current attempt is a retry after a failed attempt. + * @return A promise that resolves with the successful response. + */ + protected async requestAsync( + opts: GaxiosOptions, + retry = false + ): Promise> { + let response: GaxiosResponse; + try { + const requestHeaders = await this.getRequestHeaders(); + opts.headers = opts.headers || {}; + if (requestHeaders && requestHeaders['x-goog-user-project']) { + opts.headers['x-goog-user-project'] = + requestHeaders['x-goog-user-project']; + } + if (requestHeaders && requestHeaders.Authorization) { + opts.headers.Authorization = requestHeaders.Authorization; + } + response = await this.transporter.request(opts); + } catch (e) { + const res = (e as GaxiosError).response; + if (res) { + const statusCode = res.status; + // Retry the request for metadata if the following criteria are true: + // - We haven't already retried. It only makes sense to retry once. + // - The response was a 401 or a 403 + // - The request didn't send a readableStream + // - forceRefreshOnFailure is true + const isReadableStream = res.config.data instanceof stream.Readable; + const isAuthErr = statusCode === 401 || statusCode === 403; + if ( + !retry && + isAuthErr && + !isReadableStream && + this.forceRefreshOnFailure + ) { + await this.refreshAccessTokenAsync(); + return await this.requestAsync(opts, true); + } + } + throw e; + } + return response; } /** diff --git a/src/index.ts b/src/index.ts index 35cf959c..4d6519e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,6 +57,10 @@ export { BaseExternalAccountClient, BaseExternalAccountClientOptions, } from './auth/baseexternalclient'; +export { + CredentialAccessBoundary, + DownscopedClient, +} from './auth/downscopedclient'; export {DefaultTransporter} from './transporters'; const auth = new GoogleAuth(); diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index c41a0945..1b70b4e2 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -17,7 +17,7 @@ import {describe, it, beforeEach, afterEach} from 'mocha'; import * as nock from 'nock'; import * as sinon from 'sinon'; -import {GaxiosOptions, GaxiosPromise} from 'gaxios'; +import {GaxiosError, GaxiosOptions, GaxiosPromise} from 'gaxios'; import {Credentials} from '../src/auth/credentials'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; import { @@ -268,6 +268,31 @@ describe('DownscopedClient', () => { }); }); + it('should not throw with an optional quota_project_id', () => { + const quotaProjectId = 'quota_project_id'; + const cabWithOneAccessBoundaryRule = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: testAvailableResource, + availablePermissions: [testAvailablePermission1], + availabilityCondition: { + expression: testAvailabilityConditionExpression, + }, + }, + ], + }, + }; + assert.doesNotThrow(() => { + return new DownscopedClient( + client, + cabWithOneAccessBoundaryRule, + undefined, + quotaProjectId + ); + }); + }); + it('should set custom RefreshOptions', () => { const refreshOptions = { eagerRefreshThresholdMillis: 5000, @@ -683,30 +708,562 @@ describe('DownscopedClient', () => { }); describe('getRequestHeader()', () => { - it('should return unimplemented error when calling getRequestHeader()', async () => { - const expectedError = new Error('Not implemented.'); + it('should inject the authorization headers', async () => { + const expectedHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + const actualHeaders = await cabClient.getRequestHeaders(); + + assert.deepStrictEqual(actualHeaders, expectedHeaders); + scope.done(); + }); + + it('should inject the authorization and metadata headers', async () => { + const quotaProjectId = 'QUOTA_PROJECT_ID'; + const expectedHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + 'x-goog-user-project': quotaProjectId, + }; + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + + const cabClient = new DownscopedClient( + client, + testClientAccessBoundary, + undefined, + quotaProjectId + ); + const actualHeaders = await cabClient.getRequestHeaders(); + + assert.deepStrictEqual(expectedHeaders, actualHeaders); + scope.done(); + }); + + it('should reject when error occurs during token retrieval', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + const scope = mockStsTokenExchange([ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); - await assert.rejects(cabClient.getRequestHeaders(), expectedError); + await assert.rejects( + cabClient.getRequestHeaders(), + getErrorFromOAuthErrorResponse(errorResponse) + ); + scope.done(); }); }); describe('request()', () => { - it('should return unimplemented error when request with opts', () => { + it('should process HTTP request with authorization header', async () => { + const quotaProjectId = 'QUOTA_PROJECT_ID'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + 'x-goog-user-project': quotaProjectId, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const cabClient = new DownscopedClient( + client, + testClientAccessBoundary, + undefined, + quotaProjectId + ); + const actualResponse = await cabClient.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should process headerless HTTP request', async () => { + const quotaProjectId = 'QUOTA_PROJECT_ID'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + 'x-goog-user-project': quotaProjectId, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const cabClient = new DownscopedClient( + client, + testClientAccessBoundary, + undefined, + quotaProjectId + ); + // Send request with no headers. + const actualResponse = await cabClient.request({ + url: 'https://example.com/api', + method: 'POST', + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should reject when error occurs during token retrieval', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const scope = mockStsTokenExchange([ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + await assert.rejects( + cabClient.request({ + url: 'https://example.com/api', + method: 'POST', + data: exampleRequest, + responseType: 'json', + }), + getErrorFromOAuthErrorResponse(errorResponse) + ); + scope.done(); + }); + + it('should trigger callback on success when provided', done => { + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; const exampleRequest = { key1: 'value1', key2: 'value2', }; - const expectedError = new Error('Not implemented.'); + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; - assert.throws(() => { - return cabClient.request({ + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + cabClient.request( + { url: 'https://example.com/api', method: 'POST', + headers: exampleHeaders, data: exampleRequest, responseType: 'json', - }); - }, expectedError); + }, + (err, result) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(result?.data, exampleResponse); + scopes.forEach(scope => scope.done()); + done(); + } + ); + }); + + it('should trigger callback on error when provided', done => { + const errorMessage = 'Bad Request'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(400, errorMessage), + ]; + + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + cabClient.request( + { + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }, + (err, result) => { + assert.strictEqual(err!.message, errorMessage); + assert.deepStrictEqual(result, (err as GaxiosError)!.response); + scopes.forEach(scope => scope.done()); + done(); + } + ); + }); + + it('should retry on 401 on forceRefreshOnFailure=true', async () => { + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'DOWNSCOPED_CLIENT_ACCESS_TOKEN_1'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const authHeaders2 = { + Authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse: SampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(401) + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders2), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const cabClient = new DownscopedClient(client, testClientAccessBoundary, { + forceRefreshOnFailure: true, + }); + const actualResponse = await cabClient.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should not retry on 401 on forceRefreshOnFailure=false', async () => { + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(401), + ]; + + const cabClient = new DownscopedClient(client, testClientAccessBoundary, { + forceRefreshOnFailure: false, + }); + await assert.rejects( + cabClient.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }), + { + code: '401', + } + ); + + scopes.forEach(scope => scope.done()); + }); + + it('should not retry more than once', async () => { + const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); + stsSuccessfulResponse2.access_token = 'DOWNSCOPED_CLIENT_ACCESS_TOKEN_1'; + const authHeaders = { + Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }; + const authHeaders2 = { + Authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + { + statusCode: 200, + response: stsSuccessfulResponse2, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(403) + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders2), + }) + .reply(403), + ]; + + const cabClient = new DownscopedClient(client, testClientAccessBoundary, { + forceRefreshOnFailure: true, + }); + await assert.rejects( + cabClient.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }), + { + code: '403', + } + ); + scopes.forEach(scope => scope.done()); }); }); }); diff --git a/test/test.index.ts b/test/test.index.ts index 3d9b0457..822eda1c 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -42,6 +42,7 @@ describe('index', () => { assert(gal.IdentityPoolClient); assert(gal.AwsClient); assert(gal.BaseExternalAccountClient); + assert(gal.DownscopedClient); assert(gal.Impersonated); }); }); From 883cf2596664b7de8159fb29a8f16705218a2ad4 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 2 Sep 2021 21:18:46 +0000 Subject: [PATCH 288/662] chore: release 7.9.0 (#1248) :robot: I have created a release \*beep\* \*boop\* --- ## [7.9.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.8.0...v7.9.0) (2021-09-02) ### Features * wire up implementation of DownscopedClient. ([#1232](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1232)) ([baa1318](https://www.github.com/googleapis/google-auth-library-nodejs/commit/baa13185836bb299b769c034bfb7d37231eea8d1)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8111f37..d54603e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.9.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.8.0...v7.9.0) (2021-09-02) + + +### Features + +* wire up implementation of DownscopedClient. ([#1232](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1232)) ([baa1318](https://www.github.com/googleapis/google-auth-library-nodejs/commit/baa13185836bb299b769c034bfb7d37231eea8d1)) + ## [7.8.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.7.0...v7.8.0) (2021-08-30) diff --git a/package.json b/package.json index 16130770..a502464b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.8.0", + "version": "7.9.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index f02b70d2..3e159fad 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.8.0", + "google-auth-library": "^7.9.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 2c72de4ef7c07ddb4586094faf117715ab50143e Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Thu, 2 Sep 2021 19:07:32 -0400 Subject: [PATCH 289/662] fix(build): switch to main branch (#1249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(build): switch to main branch * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * config: update master to main Co-authored-by: Owl Bot --- .github/generated-files-bot.yml | 4 +-- .github/workflows/ci.yaml | 2 +- .readme-partials.yaml | 8 +++--- README.md | 46 ++++++++++++++++----------------- samples/README.md | 28 ++++++++++---------- src/auth/oauth2client.ts | 4 +-- 6 files changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/generated-files-bot.yml b/.github/generated-files-bot.yml index 7bb7ce54..992ccef4 100644 --- a/.github/generated-files-bot.yml +++ b/.github/generated-files-bot.yml @@ -8,9 +8,9 @@ generatedFiles: - path: '.github/generated-files-bot.+(yml|yaml)' message: '`.github/generated-files-bot.(yml|yaml)` should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' - path: 'README.md' - message: '`README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/master/.readme-partials.yaml' + message: '`README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/main/.readme-partials.yaml' - path: 'samples/README.md' - message: '`samples/README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/master/.readme-partials.yaml' + message: '`samples/README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/main/.readme-partials.yaml' ignoreAuthors: - 'gcf-owl-bot[bot]' - 'yoshi-automation' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f033c0d2..36dbfb21 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,7 @@ on: push: branches: - - master + - main pull_request: name: ci jobs: diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 3621ce5d..6f47fb10 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -252,7 +252,7 @@ body: |- main().catch(console.error); ``` - The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/jwt.js). + The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js). #### Loading credentials from environment variables Instead of loading credentials from a key file, you can also provide them using an environment variable and the `GoogleAuth.fromJSON()` method. This is particularly convenient for systems that deploy directly from source control (Heroku, App Engine, etc). @@ -558,7 +558,7 @@ body: |- main().catch(console.error); ``` - A complete example can be found in [`samples/idtokens-serverless.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). + A complete example can be found in [`samples/idtokens-serverless.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js). For invoking Cloud Identity-Aware Proxy, you will need to pass the Client ID used when you set up your protected resource as the target audience. @@ -579,7 +579,7 @@ body: |- main().catch(console.error); ``` - A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). + A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js). ### Verifying ID Tokens @@ -606,7 +606,7 @@ body: |- console.log(ticket) ``` - A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). + A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken-iap.js). ## Impersonated Credentials Client diff --git a/README.md b/README.md index 15f9ffab..f5804c37 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![release level](https://img.shields.io/badge/release%20level-general%20availability%20%28GA%29-brightgreen.svg?style=flat)](https://cloud.google.com/terms/launch-stages) [![npm version](https://img.shields.io/npm/v/google-auth-library.svg)](https://www.npmjs.org/package/google-auth-library) -[![codecov](https://img.shields.io/codecov/c/github/googleapis/google-auth-library-nodejs/master.svg?style=flat)](https://codecov.io/gh/googleapis/google-auth-library-nodejs) +[![codecov](https://img.shields.io/codecov/c/github/googleapis/google-auth-library-nodejs/main.svg?style=flat)](https://codecov.io/gh/googleapis/google-auth-library-nodejs) @@ -15,7 +15,7 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra A comprehensive list of changes in each version may be found in -[the CHANGELOG](https://github.com/googleapis/google-auth-library-nodejs/blob/master/CHANGELOG.md). +[the CHANGELOG](https://github.com/googleapis/google-auth-library-nodejs/blob/main/CHANGELOG.md). * [Google Auth Library Node.js Client API Reference][client-docs] * [Google Auth Library Documentation][product-docs] @@ -297,7 +297,7 @@ async function main() { main().catch(console.error); ``` -The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/jwt.js). +The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js). #### Loading credentials from environment variables Instead of loading credentials from a key file, you can also provide them using an environment variable and the `GoogleAuth.fromJSON()` method. This is particularly convenient for systems that deploy directly from source control (Heroku, App Engine, etc). @@ -603,7 +603,7 @@ async function main() { main().catch(console.error); ``` -A complete example can be found in [`samples/idtokens-serverless.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). +A complete example can be found in [`samples/idtokens-serverless.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js). For invoking Cloud Identity-Aware Proxy, you will need to pass the Client ID used when you set up your protected resource as the target audience. @@ -624,7 +624,7 @@ async function main() main().catch(console.error); ``` -A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). +A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js). ### Verifying ID Tokens @@ -651,7 +651,7 @@ const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( console.log(ticket) ``` -A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). +A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken-iap.js). ## Impersonated Credentials Client @@ -724,24 +724,24 @@ main(); ## Samples -Samples are in the [`samples/`](https://github.com/googleapis/google-auth-library-nodejs/tree/master/samples) directory. Each sample's `README.md` has instructions for running its sample. +Samples are in the [`samples/`](https://github.com/googleapis/google-auth-library-nodejs/tree/main/samples) directory. Each sample's `README.md` has instructions for running its sample. | Sample | Source Code | Try it | | --------------------------- | --------------------------------- | ------ | -| Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/adc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/adc.js,samples/README.md) | -| Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/compute.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) | -| Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/credentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/credentials.js,samples/README.md) | -| Headers | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/headers.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/headers.js,samples/README.md) | -| ID Tokens for Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) | -| ID Tokens for Serverless | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) | -| Jwt | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/jwt.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/jwt.js,samples/README.md) | -| Keepalive | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/keepalive.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/keepalive.js,samples/README.md) | -| Keyfile | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/keyfile.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/keyfile.js,samples/README.md) | -| Oauth2-code Verifier | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2-codeVerifier.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2-codeVerifier.js,samples/README.md) | -| Oauth2 | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2.js,samples/README.md) | -| Sign Blob | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/signBlob.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlob.js,samples/README.md) | -| Verifying ID Tokens from Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken-iap.js,samples/README.md) | -| Verify Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken.js,samples/README.md) | +| Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/adc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/adc.js,samples/README.md) | +| Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) | +| Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/credentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/credentials.js,samples/README.md) | +| Headers | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/headers.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/headers.js,samples/README.md) | +| ID Tokens for Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) | +| ID Tokens for Serverless | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) | +| Jwt | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/jwt.js,samples/README.md) | +| Keepalive | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/keepalive.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/keepalive.js,samples/README.md) | +| Keyfile | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/keyfile.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/keyfile.js,samples/README.md) | +| Oauth2-code Verifier | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2-codeVerifier.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2-codeVerifier.js,samples/README.md) | +| Oauth2 | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2.js,samples/README.md) | +| Sign Blob | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlob.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlob.js,samples/README.md) | +| Verifying ID Tokens from Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken-iap.js,samples/README.md) | +| Verify Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken.js,samples/README.md) | @@ -790,7 +790,7 @@ More Information: [Google Cloud Platform Launch Stages][launch_stages] ## Contributing -Contributions welcome! See the [Contributing Guide](https://github.com/googleapis/google-auth-library-nodejs/blob/master/CONTRIBUTING.md). +Contributions welcome! See the [Contributing Guide](https://github.com/googleapis/google-auth-library-nodejs/blob/main/CONTRIBUTING.md). Please note that this `README.md`, the `samples/README.md`, and a variety of configuration files in this repository (including `.nycrc` and `tsconfig.json`) @@ -802,7 +802,7 @@ to its templates in Apache Version 2.0 -See [LICENSE](https://github.com/googleapis/google-auth-library-nodejs/blob/master/LICENSE) +See [LICENSE](https://github.com/googleapis/google-auth-library-nodejs/blob/main/LICENSE) [client-docs]: https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest [product-docs]: https://cloud.google.com/docs/authentication/ diff --git a/samples/README.md b/samples/README.md index c4f4e5be..0729eae1 100644 --- a/samples/README.md +++ b/samples/README.md @@ -44,7 +44,7 @@ Before running the samples, make sure you've followed the steps outlined in ### Adc -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/adc.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/adc.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/adc.js,samples/README.md) @@ -61,7 +61,7 @@ __Usage:__ ### Compute -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/compute.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) @@ -78,7 +78,7 @@ __Usage:__ ### Credentials -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/credentials.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/credentials.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/credentials.js,samples/README.md) @@ -95,7 +95,7 @@ __Usage:__ ### Headers -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/headers.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/headers.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/headers.js,samples/README.md) @@ -114,7 +114,7 @@ __Usage:__ Requests an IAP-protected resource with an ID Token. -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) @@ -133,7 +133,7 @@ __Usage:__ Requests a Cloud Run or Cloud Functions URL with an ID Token. -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) @@ -150,7 +150,7 @@ __Usage:__ ### Jwt -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/jwt.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/jwt.js,samples/README.md) @@ -167,7 +167,7 @@ __Usage:__ ### Keepalive -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/keepalive.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/keepalive.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/keepalive.js,samples/README.md) @@ -184,7 +184,7 @@ __Usage:__ ### Keyfile -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/keyfile.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/keyfile.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/keyfile.js,samples/README.md) @@ -201,7 +201,7 @@ __Usage:__ ### Oauth2-code Verifier -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2-codeVerifier.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2-codeVerifier.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2-codeVerifier.js,samples/README.md) @@ -218,7 +218,7 @@ __Usage:__ ### Oauth2 -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2.js,samples/README.md) @@ -235,7 +235,7 @@ __Usage:__ ### Sign Blob -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/signBlob.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlob.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlob.js,samples/README.md) @@ -254,7 +254,7 @@ __Usage:__ Verifying the signed token from the header of an IAP-protected resource. -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken-iap.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken-iap.js,samples/README.md) @@ -271,7 +271,7 @@ __Usage:__ ### Verify Id Token -View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken.js). +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken.js). [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken.js,samples/README.md) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 9566cc12..0fdf8dcf 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -31,7 +31,7 @@ import {LoginTicket, TokenPayload} from './loginticket'; /** * The results from the `generateCodeVerifierAsync` method. To learn more, * See the sample: - * https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2-codeVerifier.js + * https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2-codeVerifier.js */ export interface CodeVerifierResults { /** @@ -565,7 +565,7 @@ export class OAuth2Client extends AuthClient { * code_challenge_method. * * For a full example see: - * https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/oauth2-codeVerifier.js + * https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2-codeVerifier.js */ async generateCodeVerifierAsync(): Promise { // base64 encoding uses 6 bits per character, and we want to generate128 From 458be7e7e35b27889e7383b5fb1a2df475d7ee76 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 2 Sep 2021 23:16:11 +0000 Subject: [PATCH 290/662] chore: release 7.9.1 (#1250) :robot: I have created a release \*beep\* \*boop\* --- ### [7.9.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.0...v7.9.1) (2021-09-02) ### Bug Fixes * **build:** switch to main branch ([#1249](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1249)) ([2c72de4](https://www.github.com/googleapis/google-auth-library-nodejs/commit/2c72de4ef7c07ddb4586094faf117715ab50143e)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d54603e4..cc574010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.9.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.0...v7.9.1) (2021-09-02) + + +### Bug Fixes + +* **build:** switch to main branch ([#1249](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1249)) ([2c72de4](https://www.github.com/googleapis/google-auth-library-nodejs/commit/2c72de4ef7c07ddb4586094faf117715ab50143e)) + ## [7.9.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.8.0...v7.9.0) (2021-09-02) diff --git a/package.json b/package.json index a502464b..b619791c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.9.0", + "version": "7.9.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 3e159fad..4e8b42d9 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^0.2.0", - "google-auth-library": "^7.9.0", + "google-auth-library": "^7.9.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From dcacc884ab7b9ad8f929dd26748400a73fe6b5e0 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 13 Sep 2021 16:01:47 -0700 Subject: [PATCH 291/662] build: enable release-trigger bot (#1212) (#1252) Source-Link: https://github.com/googleapis/synthtool/commit/0a1b7017dec842ffe94894129c757115a8f82ae9 Post-Processor: gcr.io/repo-automation-bots/owlbot-nodejs:latest@sha256:111973c0da7608bf1e60d070e5449d48826c385a6b92a56cb9203f1725d33c3d Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 2 +- .github/release-trigger.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .github/release-trigger.yml diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index c45b2393..73bbf7d3 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-nodejs:latest - digest: sha256:667a9e46a9aa5b80240ad164d55ac33bc9d6780b5ef42f125a41f0ad95bc1950 + digest: sha256:111973c0da7608bf1e60d070e5449d48826c385a6b92a56cb9203f1725d33c3d diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml new file mode 100644 index 00000000..d4ca9418 --- /dev/null +++ b/.github/release-trigger.yml @@ -0,0 +1 @@ +enabled: true From 59b82436d6b9b68b6ae0e0e81d4b797d964acae2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 16 Sep 2021 16:32:06 +0200 Subject: [PATCH 292/662] fix(deps): update dependency @googleapis/iam to v2 (#1253) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 4e8b42d9..5b16dae6 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@googleapis/iam": "^0.2.0", + "@googleapis/iam": "^2.0.0", "google-auth-library": "^7.9.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", From f11eeee28e35e81baab0668053d4ede9fb5e71c9 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 16 Sep 2021 14:38:11 +0000 Subject: [PATCH 293/662] chore: release 7.9.2 (#1254) :robot: I have created a release \*beep\* \*boop\* --- ### [7.9.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.1...v7.9.2) (2021-09-16) ### Bug Fixes * **deps:** update dependency @googleapis/iam to v2 ([#1253](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1253)) ([59b8243](https://www.github.com/googleapis/google-auth-library-nodejs/commit/59b82436d6b9b68b6ae0e0e81d4b797d964acae2)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc574010..28fe68ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.9.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.1...v7.9.2) (2021-09-16) + + +### Bug Fixes + +* **deps:** update dependency @googleapis/iam to v2 ([#1253](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1253)) ([59b8243](https://www.github.com/googleapis/google-auth-library-nodejs/commit/59b82436d6b9b68b6ae0e0e81d4b797d964acae2)) + ### [7.9.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.0...v7.9.1) (2021-09-02) diff --git a/package.json b/package.json index b619791c..402b4da2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.9.1", + "version": "7.9.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 5b16dae6..489c6132 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.9.1", + "google-auth-library": "^7.9.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 2ac7df9909b1f6ead1309aebc2288dada879c2f8 Mon Sep 17 00:00:00 2001 From: Jeffrey Rennie Date: Tue, 21 Sep 2021 14:40:29 -0700 Subject: [PATCH 294/662] chore: relocate owl bot post processor (#1296) chore: relocate owl bot post processor --- .github/.OwlBot.lock.yaml | 2 +- .github/.OwlBot.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 73bbf7d3..7d4006e7 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: - image: gcr.io/repo-automation-bots/owlbot-nodejs:latest + image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest digest: sha256:111973c0da7608bf1e60d070e5449d48826c385a6b92a56cb9203f1725d33c3d diff --git a/.github/.OwlBot.yaml b/.github/.OwlBot.yaml index 3a281cc9..8ddb67c5 100644 --- a/.github/.OwlBot.yaml +++ b/.github/.OwlBot.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. docker: - image: gcr.io/repo-automation-bots/owlbot-nodejs:latest + image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest begin-after-commit-hash: 397c0bfd367a2427104f988d5329bc117caafd95 From fe29e384820f1c97ca62478c55813aad3f8ecbea Mon Sep 17 00:00:00 2001 From: Xin Li Date: Tue, 28 Sep 2021 12:32:31 -0700 Subject: [PATCH 295/662] feat: add workforce config support. (#1251) See go/workforce-pools-client-support. Add support of work_force_user_config field for workforce pool and related logic in ExternalClient(calling constructor for IdentityPoolClient) BaseExternalClient(the parent class of IdentityPoolClient). The logic change is only related to refreshAccessToken() flow, since this API is non-public, we use getAccessToken() to test the flow instead. Add 16 tests: ExternalClient: 1. fromJson() for IdentityPoolClient, throw an error is work_force_user_project provided by audience is not workforce audience. 2. fromJson() For IdentityPoolClient, return expected response is work_force_user_project provided and audience is workforce audience. BaseExternalClient: 1. getAccessToken() Should apply basic auth on workforce configs with client auth provided(no impersonation). 2. getAccessToken() Should apply work_force_user_project on workforce configs without client auth(no impersonation). 3. getAccessToken() Should not throw if workforce audience and client auth but work_force_user_project not provided(no impersonation). 4. getAccessToken() Should not throw if workforce audience and no client auth but work_force_user_project provided( impersonation). 5. Constructor(), throw an error is work_force_user_project provided by audience is not workforce audience. 6. Constructor(), return expected response is work_force_user_project provided and audience is workforce audience. 7. getProjectId(), should resolve with workforce projectID if no client auth not and workforce user project are defined. 8. getProjectId(), should not pass workforce user project if client auth is defined. IdentityPoolClient: 1. getAccessToken() Should apply basic auth on workforce configs with client auth provided(no impersonation). 2. getAccessToken() Should apply work_force_user_project on workforce configs without client auth(no impersonation). 3. getAccessToken() Should not throw if workforce audience and client auth but work_force_user_project not provided(no impersonation). 4. getAccessToken() Should not throw if workforce audience and no client auth but work_force_user_project provided( impersonation). 5. Constructor(), throw an error is work_force_user_project provided by audience is not workforce audience. 6. Constructor(), return expected response is work_force_user_project provided and audience is workforce audience. --- src/auth/baseexternalclient.ts | 44 ++++- src/auth/identitypoolclient.ts | 3 +- test/test.baseexternalclient.ts | 316 ++++++++++++++++++++++++++++++++ test/test.externalclient.ts | 66 +++++++ test/test.identitypoolclient.ts | 259 +++++++++++++++++++++++++- 5 files changed, 679 insertions(+), 9 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index f407ad23..691e3964 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -57,6 +57,9 @@ export const EXTERNAL_ACCOUNT_TYPE = 'external_account'; /** Cloud resource manager URL used to retrieve project information. */ export const CLOUD_RESOURCE_MANAGER = 'https://cloudresourcemanager.googleapis.com/v1/projects/'; +/** The workforce audience pattern. */ +const WORKFORCE_AUDIENCE_PATTERN = + '//iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; /** * Base external account credentials json interface. @@ -71,6 +74,7 @@ export interface BaseExternalAccountClientOptions { client_id?: string; client_secret?: string; quota_project_id?: string; + workforce_pool_user_project?: string; } /** @@ -127,6 +131,8 @@ export abstract class BaseExternalAccountClient extends AuthClient { private readonly subjectTokenType: string; private readonly serviceAccountImpersonationUrl?: string; private readonly stsCredential: sts.StsCredentials; + private readonly clientAuth?: ClientAuthentication; + private readonly workforcePoolUserProject?: string; public projectId: string | null; public projectNumber: string | null; public readonly eagerRefreshThresholdMillis: number; @@ -152,7 +158,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { `received "${options.type}"` ); } - const clientAuth = options.client_id + this.clientAuth = options.client_id ? ({ confidentialClientType: 'basic', clientId: options.client_id, @@ -162,13 +168,27 @@ export abstract class BaseExternalAccountClient extends AuthClient { if (!this.validateGoogleAPIsUrl('sts', options.token_url)) { throw new Error(`"${options.token_url}" is not a valid token url.`); } - this.stsCredential = new sts.StsCredentials(options.token_url, clientAuth); + this.stsCredential = new sts.StsCredentials( + options.token_url, + this.clientAuth + ); // Default OAuth scope. This could be overridden via public property. this.scopes = [DEFAULT_OAUTH_SCOPE]; this.cachedAccessToken = null; this.audience = options.audience; this.subjectTokenType = options.subject_token_type; this.quotaProjectId = options.quota_project_id; + this.workforcePoolUserProject = options.workforce_pool_user_project; + const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN); + if ( + this.workforcePoolUserProject && + !this.audience.match(workforceAudiencePattern) + ) { + throw new Error( + 'workforcePoolUserProject should not be set for non-workforce pool ' + + 'credentials.' + ); + } if ( typeof options.service_account_impersonation_url !== 'undefined' && !this.validateGoogleAPIsUrl( @@ -290,8 +310,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { /** * @return A promise that resolves with the project ID corresponding to the - * current workload identity pool. When not determinable, this resolves with - * null. + * current workload identity pool or current workforce pool if + * determinable. For workforce pool credential, it returns the project ID + * corresponding to the workforcePoolUserProject. * This is introduced to match the current pattern of using the Auth * library: * const projectId = await auth.getProjectId(); @@ -303,15 +324,16 @@ export abstract class BaseExternalAccountClient extends AuthClient { * https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes */ async getProjectId(): Promise { + const projectNumber = this.projectNumber || this.workforcePoolUserProject; if (this.projectId) { // Return previously determined project ID. return this.projectId; - } else if (this.projectNumber) { + } else if (projectNumber) { // Preferable not to use request() to avoid retrial policies. const headers = await this.getRequestHeaders(); const response = await this.transporter.request({ headers, - url: `${CLOUD_RESOURCE_MANAGER}${this.projectNumber}`, + url: `${CLOUD_RESOURCE_MANAGER}${projectNumber}`, responseType: 'json', }); this.projectId = response.data.projectId; @@ -401,8 +423,16 @@ export abstract class BaseExternalAccountClient extends AuthClient { }; // Exchange the external credentials for a GCP access token. + // Client auth is prioritized over passing the workforcePoolUserProject + // parameter for STS token exchange. + const additionalOptions = + !this.clientAuth && this.workforcePoolUserProject + ? {userProject: this.workforcePoolUserProject} + : undefined; const stsResponse = await this.stsCredential.exchangeToken( - stsCredentialsOptions + stsCredentialsOptions, + undefined, + additionalOptions ); if (this.serviceAccountImpersonationUrl) { diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index 33badafa..eed39b1c 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -70,7 +70,8 @@ export class IdentityPoolClient extends BaseExternalAccountClient { * Instantiate an IdentityPoolClient instance using the provided JSON * object loaded from an external account credentials file. * An error is thrown if the credential is not a valid file-sourced or - * url-sourced credential. + * url-sourced credential or a workforce pool user project is provided + * with a non workforce audience. * @param options The external account options object typically loaded * from the external account JSON credential file. * @param additionalOptions Optional additional behavior customization diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 9961529a..72acd595 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -82,6 +82,24 @@ describe('BaseExternalAccountClient', () => { client_id: 'CLIENT_ID', client_secret: 'SECRET', }; + const externalAccountOptionsWorkforceUserProject = Object.assign( + {}, + externalAccountOptions, + { + workforce_pool_user_project: 'workforce_pool_user_project', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + } + ); + const externalAccountOptionsWithClientAuthAndWorkforceUserProject = + Object.assign( + { + client_id: 'CLIENT_ID', + client_secret: 'SECRET', + }, + externalAccountOptionsWorkforceUserProject + ); const basicAuthCreds = `${externalAccountOptionsWithCreds.client_id}:` + `${externalAccountOptionsWithCreds.client_secret}`; @@ -104,6 +122,12 @@ describe('BaseExternalAccountClient', () => { }, externalAccountOptionsWithCreds ); + const externalAccountOptionsWithWorkforceUserProjectAndSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + externalAccountOptionsWorkforceUserProject + ); const indeterminableProjectIdAudiences = [ // Legacy K8s audience format. 'identitynamespace:1f12345:my_provider', @@ -245,6 +269,64 @@ describe('BaseExternalAccountClient', () => { } }); + const invalidWorkforceAudiences = [ + '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcepools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools//providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/providers/provider', + '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/provider', + '//iam.googleapis.com//locations/global/workforcePools/pool/providers/provider', + '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers', + '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', + '//iam.googleapis.com/locations//workforcePools/pool/providers/provider', + '//iam.googleapis.com/locations/workforcePools/pool/providers/provider', + ]; + const invalidExternalAccountOptionsWorkforceUserProject = Object.assign( + {}, + externalAccountOptionsWorkforceUserProject + ); + const expectedWorkforcePoolUserProjectError = new Error( + 'workforcePoolUserProject should not be set for non-workforce pool ' + + 'credentials.' + ); + + invalidWorkforceAudiences.forEach(invalidWorkforceAudience => { + it(`should throw given audience ${invalidWorkforceAudience} with user project defined in options`, () => { + invalidExternalAccountOptionsWorkforceUserProject.audience = + invalidWorkforceAudience; + + assert.throws(() => { + return new TestExternalAccountClient( + invalidExternalAccountOptionsWorkforceUserProject + ); + }, expectedWorkforcePoolUserProjectError); + }); + }); + + it('should not throw on valid workforce audience configs', () => { + const validWorkforceAudiences = [ + '//iam.googleapis.com/locations/global/workforcePools/workforcePools/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/workloadPools/providers/oidc', + ]; + const validExternalAccountOptionsWorkforceUserProject = Object.assign( + {}, + externalAccountOptionsWorkforceUserProject + ); + for (const validWorkforceAudience of validWorkforceAudiences) { + validExternalAccountOptionsWorkforceUserProject.audience = + validWorkforceAudience; + + assert.doesNotThrow(() => { + return new TestExternalAccountClient( + validExternalAccountOptionsWorkforceUserProject + ); + }); + } + }); + it('should not throw on valid options', () => { assert.doesNotThrow(() => { return new TestExternalAccountClient(externalAccountOptions); @@ -280,6 +362,16 @@ describe('BaseExternalAccountClient', () => { }); describe('projectNumber', () => { + it('should return null for workforce pools with workforce_pool_user_project', () => { + const options = Object.assign( + {}, + externalAccountOptionsWorkforceUserProject + ); + const client = new TestExternalAccountClient(options); + + assert(client.projectNumber === null); + }); + it('should be set if determinable', () => { const projectNumber = 'my-proj-number'; const options = Object.assign({}, externalAccountOptions); @@ -342,6 +434,65 @@ describe('BaseExternalAccountClient', () => { }); describe('getProjectId()', () => { + it('should resolve for workforce pools when workforce_pool_user_project is provided', async () => { + const options = Object.assign( + {}, + externalAccountOptionsWorkforceUserProject + ); + const projectNumber = options.workforce_pool_user_project; + const projectId = 'my-proj-id'; + const response = { + projectNumber, + projectId, + lifecycleState: 'ACTIVE', + name: 'project-name', + createTime: '2018-11-06T04:42:54.109Z', + parent: { + type: 'folder', + id: '12345678901', + }, + }; + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + options: JSON.stringify({ + userProject: options.workforce_pool_user_project, + }), + }, + }, + ]), + mockCloudResourceManager( + options.workforce_pool_user_project, + stsSuccessfulResponse.access_token, + 200, + response + ), + ]; + + const client = new TestExternalAccountClient(options); + const actualProjectId = await client.getProjectId(); + + assert.strictEqual(actualProjectId, projectId); + assert.strictEqual(client.projectId, projectId); + + // Next call should return cached result. + const cachedProjectId = await client.getProjectId(); + + assert.strictEqual(cachedProjectId, projectId); + scopes.forEach(scope => scope.done()); + }); + it('should resolve with projectId when determinable', async () => { const projectNumber = 'my-proj-number'; const projectId = 'my-proj-id'; @@ -484,6 +635,122 @@ describe('BaseExternalAccountClient', () => { scope.done(); }); + it('should use client auth over passing the workforce user project when both are provided', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + }, + additionalHeaders: { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + }, + }, + ]); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithClientAuthAndWorkforceUserProject + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should pass the workforce user project on workforce configs when client auth is not provided ', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + options: JSON.stringify({ + userProject: + externalAccountOptionsWorkforceUserProject.workforce_pool_user_project, + }), + }, + }, + ]); + + const client = new TestExternalAccountClient( + externalAccountOptionsWorkforceUserProject + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should not throw if client auth is provided but workforce user project is not', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + }, + additionalHeaders: { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + }, + }, + ]); + const externalAccountOptionsWithClientAuth: BaseExternalAccountClientOptions = + Object.assign( + {}, + externalAccountOptionsWithClientAuthAndWorkforceUserProject + ); + delete externalAccountOptionsWithClientAuth.workforce_pool_user_project; + + const client = new TestExternalAccountClient( + externalAccountOptionsWithClientAuth + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + it('should return credential with no expiry date if STS response does not return one', async () => { const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); const emittedEvents: Credentials[] = []; @@ -1307,6 +1574,55 @@ describe('BaseExternalAccountClient', () => { }); scopes.forEach(scope => scope.done()); }); + + it('should still pass workforce user project when no client auth is used', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + options: JSON.stringify({ + userProject: + externalAccountOptionsWithWorkforceUserProjectAndSA.workforce_pool_user_project, + }), + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithWorkforceUserProjectAndSA + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); }); }); diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index 4d1f95fe..f341d343 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -77,6 +77,21 @@ describe('ExternalAccountClient', () => { forceRefreshOnFailure: true, }; + const invalidWorkforceIdentityPoolClientAudiences = [ + '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcepools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools//providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/providers/oidc', + '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/oidc', + '//iam.googleapis.com//locations/global/workforcePools/pool/providers/oidc', + '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', + '//iam.googleapis.com/locations//workforcePools/pool/providers/oidc', + '//iam.googleapis.com/locations/workforcePools/pool/providers/oidc', + ]; + it('should return IdentityPoolClient on IdentityPoolClientOptions', () => { const expectedClient = new IdentityPoolClient(fileSourcedOptions); @@ -116,6 +131,57 @@ describe('ExternalAccountClient', () => { ); }); + it('should return an IdentityPoolClient with a workforce config', () => { + const validWorkforceIdentityPoolClientAudiences = [ + '//iam.googleapis.com/locations/global/workforcePools/workforcePools/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/workloadPools/providers/oidc', + ]; + const workforceFileSourcedOptions = Object.assign( + {}, + fileSourcedOptions, + { + workforce_pool_user_project: 'workforce_pool_user_project', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + } + ); + for (const validWorkforceIdentityPoolClientAudience of validWorkforceIdentityPoolClientAudiences) { + workforceFileSourcedOptions.audience = + validWorkforceIdentityPoolClientAudience; + const expectedClient = new IdentityPoolClient( + workforceFileSourcedOptions + ); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(workforceFileSourcedOptions), + expectedClient + ); + } + }); + + invalidWorkforceIdentityPoolClientAudiences.forEach( + invalidWorkforceIdentityPoolClientAudience => { + const workforceIdentityPoolClientInvalidOptions = Object.assign( + {}, + fileSourcedOptions, + { + workforce_pool_user_project: 'workforce_pool_user_project', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + } + ); + it(`should throw an error when an invalid workforce audience ${invalidWorkforceIdentityPoolClientAudience} is provided with a workforce user project`, () => { + workforceIdentityPoolClientInvalidOptions.audience = + invalidWorkforceIdentityPoolClientAudience; + + assert.throws(() => { + return ExternalAccountClient.fromJSON( + workforceIdentityPoolClientInvalidOptions + ); + }); + }); + } + ); + it('should return null when given non-ExternalAccountClientOptions', () => { assert( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 9543e8b7..69b97cc8 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -16,7 +16,7 @@ import * as assert from 'assert'; import {describe, it} from 'mocha'; import * as fs from 'fs'; import * as nock from 'nock'; - +import {createCrypto} from '../src/crypto/crypto'; import { IdentityPoolClient, IdentityPoolClientOptions, @@ -48,6 +48,7 @@ describe('IdentityPoolClient', () => { 'utf-8' ); const audience = getAudience(); + const crypto = createCrypto(); const fileSourcedOptions = { type: 'external_account', audience, @@ -63,6 +64,32 @@ describe('IdentityPoolClient', () => { }, fileSourcedOptions ); + const fileSourcedOptionsWithWorkforceUserProject = Object.assign( + {}, + fileSourcedOptions, + { + workforce_pool_user_project: 'workforce_pool_user_project', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + } + ); + const fileSourcedOptionsWithClientAuthAndWorkforceUserProject = Object.assign( + { + client_id: 'CLIENT_ID', + client_secret: 'SECRET', + }, + fileSourcedOptionsWithWorkforceUserProject + ); + const fileSourcedOptionsWithWorkforceUserProjectAndSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + fileSourcedOptionsWithWorkforceUserProject + ); + const basicAuthCreds = + `${fileSourcedOptionsWithClientAuthAndWorkforceUserProject.client_id}:` + + `${fileSourcedOptionsWithClientAuthAndWorkforceUserProject.client_secret}`; const jsonFileSourcedOptions: IdentityPoolClientOptions = { type: 'external_account', audience, @@ -147,6 +174,29 @@ describe('IdentityPoolClient', () => { }); describe('Constructor', () => { + const invalidWorkforceIdentityPoolClientAudiences = [ + '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcepools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools//providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/providers/oidc', + '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/oidc', + '//iam.googleapis.com//locations/global/workforcePools/pool/providers/oidc', + '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', + '//iam.googleapis.com/locations//workforcePools/pool/providers/oidc', + '//iam.googleapis.com/locations/workforcePools/pool/providers/oidc', + ]; + const invalidWorkforceIdentityPoolFileSourceOptions = Object.assign( + {}, + fileSourcedOptionsWithWorkforceUserProject + ); + const expectedWorkforcePoolUserProjectError = new Error( + 'workforcePoolUserProject should not be set for non-workforce pool ' + + 'credentials.' + ); + it('should throw when invalid options are provided', () => { const expectedError = new Error( 'No valid Identity Pool "credential_source" provided' @@ -211,6 +261,21 @@ describe('IdentityPoolClient', () => { }, expectedError); }); + invalidWorkforceIdentityPoolClientAudiences.forEach( + invalidWorkforceIdentityPoolClientAudience => { + it(`should throw given audience ${invalidWorkforceIdentityPoolClientAudience} with user project defined in IdentityPoolClientOptions`, () => { + invalidWorkforceIdentityPoolFileSourceOptions.audience = + invalidWorkforceIdentityPoolClientAudience; + + assert.throws(() => { + return new IdentityPoolClient( + invalidWorkforceIdentityPoolFileSourceOptions + ); + }, expectedWorkforcePoolUserProjectError); + }); + } + ); + it('should not throw when valid file-sourced options are provided', () => { assert.doesNotThrow(() => { return new IdentityPoolClient(fileSourcedOptions); @@ -232,6 +297,28 @@ describe('IdentityPoolClient', () => { return new IdentityPoolClient(urlSourcedOptionsNoHeaders); }); }); + + it('should not throw on valid workforce audience configs', () => { + const validWorkforceIdentityPoolClientAudiences = [ + '//iam.googleapis.com/locations/global/workforcePools/workforcePools/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/workloadPools/providers/oidc', + ]; + const validWorkforceIdentityPoolFileSourceOptions = Object.assign( + {}, + fileSourcedOptionsWithWorkforceUserProject + ); + for (const validWorkforceIdentityPoolClientAudience of validWorkforceIdentityPoolClientAudiences) { + validWorkforceIdentityPoolFileSourceOptions.audience = + validWorkforceIdentityPoolClientAudience; + + assert.doesNotThrow(() => { + return new IdentityPoolClient( + validWorkforceIdentityPoolFileSourceOptions + ); + }); + } + }); }); describe('for file-sourced subject tokens', () => { @@ -337,6 +424,176 @@ describe('IdentityPoolClient', () => { scope.done(); }); + it('should resolve with the expected response on workforce configs with client auth', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + }, + additionalHeaders: { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + }, + }, + ]); + + const client = new IdentityPoolClient( + fileSourcedOptionsWithClientAuthAndWorkforceUserProject + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should resolve with the expected response on workforce configs without client auth', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + options: JSON.stringify({ + userProject: + fileSourcedOptionsWithWorkforceUserProject.workforce_pool_user_project, + }), + }, + }, + ]); + + const client = new IdentityPoolClient( + fileSourcedOptionsWithWorkforceUserProject + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should not throw if client auth is provided but workforce user project is not', async () => { + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + }, + additionalHeaders: { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + }, + }, + ]); + const fileSourcedOptionsWithClientAuth: IdentityPoolClientOptions = + Object.assign( + {}, + fileSourcedOptionsWithClientAuthAndWorkforceUserProject + ); + delete fileSourcedOptionsWithClientAuth.workforce_pool_user_project; + + const client = new IdentityPoolClient(fileSourcedOptionsWithClientAuth); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + + it('should still pass workforce user project when impersonation and no client auth are used', async () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + options: JSON.stringify({ + userProject: + fileSourcedOptionsWithWorkforceUserProjectAndSA.workforce_pool_user_project, + }), + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const client = new IdentityPoolClient( + fileSourcedOptionsWithWorkforceUserProjectAndSA + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + it('should handle service account access token for text format', async () => { const now = new Date().getTime(); const saSuccessResponse = { From 151513131f6256d381480b49bdfadf692a336d3d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 28 Sep 2021 19:40:12 +0000 Subject: [PATCH 296/662] chore: release 7.10.0 (#1299) :robot: I have created a release \*beep\* \*boop\* --- ## [7.10.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.2...v7.10.0) (2021-09-28) ### Features * add workforce config support. ([#1251](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1251)) ([fe29e38](https://www.github.com/googleapis/google-auth-library-nodejs/commit/fe29e384820f1c97ca62478c55813aad3f8ecbea)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28fe68ce..91386d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.10.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.2...v7.10.0) (2021-09-28) + + +### Features + +* add workforce config support. ([#1251](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1251)) ([fe29e38](https://www.github.com/googleapis/google-auth-library-nodejs/commit/fe29e384820f1c97ca62478c55813aad3f8ecbea)) + ### [7.9.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.1...v7.9.2) (2021-09-16) diff --git a/package.json b/package.json index 402b4da2..a06b5edb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.9.2", + "version": "7.10.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 489c6132..6d467282 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.9.2", + "google-auth-library": "^7.10.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 006dd1943c8c007ac49f31e6d692d3661c9f4347 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 14 Oct 2021 00:45:09 +0000 Subject: [PATCH 297/662] build(node): update deps used during postprocessing (#1243) (#1303) --- .github/.OwlBot.lock.yaml | 2 +- .github/workflows/ci.yaml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 7d4006e7..8d0a479d 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:111973c0da7608bf1e60d070e5449d48826c385a6b92a56cb9203f1725d33c3d + digest: sha256:bbb8dd6576ac58830a07fc17e9511ae898be44f2219d3344449b125df9854441 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 36dbfb21..dd397054 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,4 +54,7 @@ jobs: with: node-version: 14 - run: npm install - - run: npm run docs-test + - run: npm run docs + - uses: JustinBeckwith/linkinator-action@v1 + with: + paths: docs/ From 94401a6b73eeaf370aeaf9cbf92f23f4fc7bde9b Mon Sep 17 00:00:00 2001 From: Matthew Lorimor Date: Thu, 14 Oct 2021 10:46:01 -0500 Subject: [PATCH 298/662] fix(security): explicitly update keypair dependency https://securitylab.github.com/advisories/GHSL-2021-1012-keypair/ Co-authored-by: Justin Beckwith --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a06b5edb..f79783ed 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^5.0.0", - "keypair": "^1.0.1", + "keypair": "^1.0.4", "linkinator": "^2.0.0", "mocha": "^8.0.0", "mv": "^2.1.1", From e69308f58c3972b71326ba48ad5d3f1d94200c69 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 14 Oct 2021 15:52:12 +0000 Subject: [PATCH 299/662] chore: release 7.10.1 (#1304) :robot: I have created a release \*beep\* \*boop\* --- ### [7.10.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.0...v7.10.1) (2021-10-14) ### Bug Fixes * **security:** explicitly update keypair dependency ([94401a6](https://www.github.com/googleapis/google-auth-library-nodejs/commit/94401a6b73eeaf370aeaf9cbf92f23f4fc7bde9b)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91386d53..33f99706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.10.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.0...v7.10.1) (2021-10-14) + + +### Bug Fixes + +* **security:** explicitly update keypair dependency ([94401a6](https://www.github.com/googleapis/google-auth-library-nodejs/commit/94401a6b73eeaf370aeaf9cbf92f23f4fc7bde9b)) + ## [7.10.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.9.2...v7.10.0) (2021-09-28) diff --git a/package.json b/package.json index f79783ed..2740a0fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.10.0", + "version": "7.10.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 6d467282..854ca060 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.10.0", + "google-auth-library": "^7.10.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From a6562371fd11deecc13ae00122f5ffefb67e3cbb Mon Sep 17 00:00:00 2001 From: "F. Hinkelmann" Date: Thu, 21 Oct 2021 11:28:18 -0400 Subject: [PATCH 300/662] chore(cloud-rad): delete api-extractor config (#1306) --- api-extractor.json | 369 --------------------------------------------- 1 file changed, 369 deletions(-) delete mode 100644 api-extractor.json diff --git a/api-extractor.json b/api-extractor.json deleted file mode 100644 index de228294..00000000 --- a/api-extractor.json +++ /dev/null @@ -1,369 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - - /** - * Optionally specifies another JSON config file that this file extends from. This provides a way for - * standard settings to be shared across multiple projects. - * - * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains - * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be - * resolved using NodeJS require(). - * - * SUPPORTED TOKENS: none - * DEFAULT VALUE: "" - */ - // "extends": "./shared/api-extractor-base.json" - // "extends": "my-package/include/api-extractor-base.json" - - /** - * Determines the "" token that can be used with other config file settings. The project folder - * typically contains the tsconfig.json and package.json config files, but the path is user-defined. - * - * The path is resolved relative to the folder of the config file that contains the setting. - * - * The default value for "projectFolder" is the token "", which means the folder is determined by traversing - * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder - * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error - * will be reported. - * - * SUPPORTED TOKENS: - * DEFAULT VALUE: "" - */ - // "projectFolder": "..", - - /** - * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor - * analyzes the symbols exported by this module. - * - * The file extension must be ".d.ts" and not ".ts". - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - */ - "mainEntryPointFilePath": "/protos/protos.d.ts", - - /** - * A list of NPM package names whose exports should be treated as part of this package. - * - * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", - * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part - * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly - * imports library2. To avoid this, we can specify: - * - * "bundledPackages": [ "library2" ], - * - * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been - * local files for library1. - */ - "bundledPackages": [ ], - - /** - * Determines how the TypeScript compiler engine will be invoked by API Extractor. - */ - "compiler": { - /** - * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * Note: This setting will be ignored if "overrideTsconfig" is used. - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/tsconfig.json" - */ - // "tsconfigFilePath": "/tsconfig.json", - - /** - * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. - * The object must conform to the TypeScript tsconfig schema: - * - * http://json.schemastore.org/tsconfig - * - * If omitted, then the tsconfig.json file will be read from the "projectFolder". - * - * DEFAULT VALUE: no overrideTsconfig section - */ - // "overrideTsconfig": { - // . . . - // } - - /** - * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended - * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when - * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses - * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. - * - * DEFAULT VALUE: false - */ - // "skipLibCheck": true, - }, - - /** - * Configures how the API report file (*.api.md) will be generated. - */ - "apiReport": { - /** - * (REQUIRED) Whether to generate an API report. - */ - "enabled": true, - - /** - * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce - * a full file path. - * - * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". - * - * SUPPORTED TOKENS: , - * DEFAULT VALUE: ".api.md" - */ - // "reportFileName": ".api.md", - - /** - * Specifies the folder where the API report file is written. The file name portion is determined by - * the "reportFileName" setting. - * - * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, - * e.g. for an API review. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/etc/" - */ - // "reportFolder": "/etc/", - - /** - * Specifies the folder where the temporary report file is written. The file name portion is determined by - * the "reportFileName" setting. - * - * After the temporary file is written to disk, it is compared with the file in the "reportFolder". - * If they are different, a production build will fail. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/temp/" - */ - // "reportTempFolder": "/temp/" - }, - - /** - * Configures how the doc model file (*.api.json) will be generated. - */ - "docModel": { - /** - * (REQUIRED) Whether to generate a doc model file. - */ - "enabled": true, - - /** - * The output path for the doc model file. The file extension should be ".api.json". - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/temp/.api.json" - */ - // "apiJsonFilePath": "/temp/.api.json" - }, - - /** - * Configures how the .d.ts rollup file will be generated. - */ - "dtsRollup": { - /** - * (REQUIRED) Whether to generate the .d.ts rollup file. - */ - "enabled": true, - - /** - * Specifies the output path for a .d.ts rollup file to be generated without any trimming. - * This file will include all declarations that are exported by the main entry point. - * - * If the path is an empty string, then this file will not be written. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/dist/.d.ts" - */ - // "untrimmedFilePath": "/dist/.d.ts", - - /** - * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. - * This file will include only declarations that are marked as "@public" or "@beta". - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "" - */ - // "betaTrimmedFilePath": "/dist/-beta.d.ts", - - - /** - * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. - * This file will include only declarations that are marked as "@public". - * - * If the path is an empty string, then this file will not be written. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "" - */ - // "publicTrimmedFilePath": "/dist/-public.d.ts", - - /** - * When a declaration is trimmed, by default it will be replaced by a code comment such as - * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the - * declaration completely. - * - * DEFAULT VALUE: false - */ - // "omitTrimmingComments": true - }, - - /** - * Configures how the tsdoc-metadata.json file will be generated. - */ - "tsdocMetadata": { - /** - * Whether to generate the tsdoc-metadata.json file. - * - * DEFAULT VALUE: true - */ - // "enabled": true, - - /** - * Specifies where the TSDoc metadata file should be written. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", - * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup - * falls back to "tsdoc-metadata.json" in the package folder. - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "" - */ - // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" - }, - - /** - * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files - * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. - * To use the OS's default newline kind, specify "os". - * - * DEFAULT VALUE: "crlf" - */ - // "newlineKind": "crlf", - - /** - * Configures how API Extractor reports error and warning messages produced during analysis. - * - * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. - */ - "messages": { - /** - * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing - * the input .d.ts files. - * - * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" - * - * DEFAULT VALUE: A single "default" entry with logLevel=warning. - */ - "compilerMessageReporting": { - /** - * Configures the default routing for messages that don't match an explicit rule in this table. - */ - "default": { - /** - * Specifies whether the message should be written to the the tool's output log. Note that - * the "addToApiReportFile" property may supersede this option. - * - * Possible values: "error", "warning", "none" - * - * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail - * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes - * the "--local" option), the warning is displayed but the build will not fail. - * - * DEFAULT VALUE: "warning" - */ - "logLevel": "warning", - - /** - * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), - * then the message will be written inside that file; otherwise, the message is instead logged according to - * the "logLevel" option. - * - * DEFAULT VALUE: false - */ - // "addToApiReportFile": false - }, - - // "TS2551": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - }, - - /** - * Configures handling of messages reported by API Extractor during its analysis. - * - * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" - * - * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings - */ - "extractorMessageReporting": { - "default": { - "logLevel": "warning", - // "addToApiReportFile": false - }, - - // "ae-extra-release-tag": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - }, - - /** - * Configures handling of messages reported by the TSDoc parser when analyzing code comments. - * - * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" - * - * DEFAULT VALUE: A single "default" entry with logLevel=warning. - */ - "tsdocMessageReporting": { - "default": { - "logLevel": "warning", - // "addToApiReportFile": false - } - - // "tsdoc-link-tag-unescaped-text": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - } - } - -} From 2aba1fdbf65dcaafbc28b02307b43d61944155c2 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 22 Oct 2021 10:52:40 -0700 Subject: [PATCH 301/662] test: fix compilation of system tests (#1309) Fixes #1308 --- system-test/fixtures/kitchen/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index 5540332c..b52a08b2 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -17,7 +17,7 @@ "google-auth-library": "file:./google-auth-library.tgz" }, "devDependencies": { - "@types/node": "^14.0.0", + "@types/node": "^16.11.3", "typescript": "^3.0.0", "gts": "^2.0.0", "null-loader": "^4.0.0", From 2b6816905008be74454c2697fd1e485c4061ad15 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 26 Oct 2021 23:18:33 +0200 Subject: [PATCH 302/662] chore(deps): update dependency @types/node to v16 (#1310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [@types/node](https://togithub.com/DefinitelyTyped/DefinitelyTyped) | [`^14.0.0` -> `^16.0.0`](https://renovatebot.com/diffs/npm/@types%2fnode/14.17.32/16.11.6) | [![age](https://badges.renovateapi.com/packages/npm/@types%2fnode/16.11.6/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/@types%2fnode/16.11.6/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/@types%2fnode/16.11.6/compatibility-slim/14.17.32)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/@types%2fnode/16.11.6/confidence-slim/14.17.32)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2740a0fc..56c99568 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@types/mocha": "^8.0.0", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", - "@types/node": "^14.0.0", + "@types/node": "^16.0.0", "@types/sinon": "^10.0.0", "@types/tmp": "^0.2.0", "assert-rejects": "^1.0.0", From b3ba9ac834de86022a78ac540995fa35857d6670 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 3 Nov 2021 16:32:34 +0100 Subject: [PATCH 303/662] fix(deps): update dependency puppeteer to v11 (#1312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [puppeteer](https://togithub.com/puppeteer/puppeteer) | [`^10.0.0` -> `^11.0.0`](https://renovatebot.com/diffs/npm/puppeteer/10.4.0/11.0.0) | [![age](https://badges.renovateapi.com/packages/npm/puppeteer/11.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/puppeteer/11.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/puppeteer/11.0.0/compatibility-slim/10.4.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/puppeteer/11.0.0/confidence-slim/10.4.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
puppeteer/puppeteer ### [`v11.0.0`](https://togithub.com/puppeteer/puppeteer/blob/master/CHANGELOG.md#​1100-httpsgithubcompuppeteerpuppeteercomparev1040v1100-2021-11-02) [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/v10.4.0...v11.0.0) ##### ⚠ BREAKING CHANGES - **oop iframes:** integrate OOP iframes with the frame manager ([#​7556](https://togithub.com/puppeteer/puppeteer/issues/7556)) ##### Features - improve error message for response.buffer() ([#​7669](https://togithub.com/puppeteer/puppeteer/issues/7669)) ([03c9ecc](https://togithub.com/puppeteer/puppeteer/commit/03c9ecca400a02684cd60229550dbad1190a5b6e)) - **oop iframes:** integrate OOP iframes with the frame manager ([#​7556](https://togithub.com/puppeteer/puppeteer/issues/7556)) ([4d9dc8c](https://togithub.com/puppeteer/puppeteer/commit/4d9dc8c0e613f22d4cdf237e8bd0b0da3c588edb)), closes [#​2548](https://togithub.com/puppeteer/puppeteer/issues/2548) - add custom debugging port option ([#​4993](https://togithub.com/puppeteer/puppeteer/issues/4993)) ([26145e9](https://togithub.com/puppeteer/puppeteer/commit/26145e9a24af7caed6ece61031f2cafa6abd505f)) - add initiator to HTTPRequest ([#​7614](https://togithub.com/puppeteer/puppeteer/issues/7614)) ([a271145](https://togithub.com/puppeteer/puppeteer/commit/a271145b0663ef9de1903dd0eb9fd5366465bed7)) - allow to customize tmpdir ([#​7243](https://togithub.com/puppeteer/puppeteer/issues/7243)) ([b1f6e86](https://togithub.com/puppeteer/puppeteer/commit/b1f6e8692b0bc7e8551b2a78169c830cd80a7acb)) - handle unhandled promise rejections in tests ([#​7722](https://togithub.com/puppeteer/puppeteer/issues/7722)) ([07febca](https://togithub.com/puppeteer/puppeteer/commit/07febca04b391893cfc872250e4391da142d4fe2)) ##### Bug Fixes - add support for relative install paths to BrowserFetcher ([#​7613](https://togithub.com/puppeteer/puppeteer/issues/7613)) ([eebf452](https://togithub.com/puppeteer/puppeteer/commit/eebf452d38b79bb2ea1a1ba84c3d2ea6f2f9f899)), closes [#​7592](https://togithub.com/puppeteer/puppeteer/issues/7592) - add webp to screenshot quality option allow list ([#​7631](https://togithub.com/puppeteer/puppeteer/issues/7631)) ([b20c2bf](https://togithub.com/puppeteer/puppeteer/commit/b20c2bfa24cbdd4a1b9cefca2e0a9407e442baf5)) - prevent Target closed errors on streams ([#​7728](https://togithub.com/puppeteer/puppeteer/issues/7728)) ([5b792de](https://togithub.com/puppeteer/puppeteer/commit/5b792de7a97611441777d1ac99cb95516301d7dc)) - request an animation frame to fix flaky clickablePoint test ([#​7587](https://togithub.com/puppeteer/puppeteer/issues/7587)) ([7341d9f](https://togithub.com/puppeteer/puppeteer/commit/7341d9fadd1466a5b2f2bde8631f3b02cf9a7d8a)) - setup husky properly ([#​7727](https://togithub.com/puppeteer/puppeteer/issues/7727)) ([8b712e7](https://togithub.com/puppeteer/puppeteer/commit/8b712e7b642b58193437f26d4e104a9e412f388d)), closes [#​7726](https://togithub.com/puppeteer/puppeteer/issues/7726) - updated troubleshooting.md to meet latest dependencies changes ([#​7656](https://togithub.com/puppeteer/puppeteer/issues/7656)) ([edb0197](https://togithub.com/puppeteer/puppeteer/commit/edb01972b9606d8b05b979a588eda0d622315981)) - **launcher:** launcher.launch() should pass 'timeout' option [#​5180](https://togithub.com/puppeteer/puppeteer/issues/5180) ([#​7596](https://togithub.com/puppeteer/puppeteer/issues/7596)) ([113489d](https://togithub.com/puppeteer/puppeteer/commit/113489d3b58e2907374a4e6e5133bf46630695d1)) - **page:** fallback to default in exposeFunction when using imported module ([#​6365](https://togithub.com/puppeteer/puppeteer/issues/6365)) ([44c9ec6](https://togithub.com/puppeteer/puppeteer/commit/44c9ec67c57dccf3e186c86f14f3a8da9a8eb971)) - **page:** fix page.off method for request event ([#​7624](https://togithub.com/puppeteer/puppeteer/issues/7624)) ([d0cb943](https://togithub.com/puppeteer/puppeteer/commit/d0cb9436a302418086f6763e0e58ae3732a20b62)), closes [#​7572](https://togithub.com/puppeteer/puppeteer/issues/7572)
--- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 56c99568..45e680dd 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^10.0.0", + "puppeteer": "^11.0.0", "sinon": "^11.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 1c350860..3532d06f 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^7.0.0", - "puppeteer": "^10.0.0" + "puppeteer": "^11.0.0" } } From ea025845215e534ad99b878399700272d2b70be3 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 15:42:19 +0000 Subject: [PATCH 304/662] chore: release 7.10.2 (#1313) :robot: I have created a release \*beep\* \*boop\* --- ### [7.10.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.1...v7.10.2) (2021-11-03) ### Bug Fixes * **deps:** update dependency puppeteer to v11 ([#1312](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1312)) ([b3ba9ac](https://www.github.com/googleapis/google-auth-library-nodejs/commit/b3ba9ac834de86022a78ac540995fa35857d6670)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f99706..6d074c16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.10.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.1...v7.10.2) (2021-11-03) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v11 ([#1312](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1312)) ([b3ba9ac](https://www.github.com/googleapis/google-auth-library-nodejs/commit/b3ba9ac834de86022a78ac540995fa35857d6670)) + ### [7.10.1](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.0...v7.10.1) (2021-10-14) diff --git a/package.json b/package.json index 45e680dd..c62f6381 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.10.1", + "version": "7.10.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 854ca060..5f908d04 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.10.1", + "google-auth-library": "^7.10.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 65044310b6d42da933437b13be954ae5dc331bc7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 4 Nov 2021 20:40:29 +0100 Subject: [PATCH 305/662] chore(deps): update dependency sinon to v12 (#1314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [sinon](https://sinonjs.org/) ([source](https://togithub.com/sinonjs/sinon)) | [`^11.0.0` -> `^12.0.0`](https://renovatebot.com/diffs/npm/sinon/11.1.2/12.0.1) | [![age](https://badges.renovateapi.com/packages/npm/sinon/12.0.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/sinon/12.0.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/sinon/12.0.1/compatibility-slim/11.1.2)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/sinon/12.0.1/confidence-slim/11.1.2)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
sinonjs/sinon ### [`v12.0.1`](https://togithub.com/sinonjs/sinon/blob/master/CHANGES.md#​1201) [Compare Source](https://togithub.com/sinonjs/sinon/compare/v12.0.0...v12.0.1) - [`3f598221`](https://togithub.com/sinonjs/sinon/commit/3f598221045904681f2b3b3ba1df617ed5e230e3) Fix issue with npm unlink for npm version > 6 (Carl-Erik Kopseng) > 'npm unlink' would implicitly unlink the current dir > until version 7, which requires an argument - [`51417a38`](https://togithub.com/sinonjs/sinon/commit/51417a38111eeeb7cd14338bfb762cc2df487e1b) Fix bundling of cjs module ([#​2412](https://togithub.com/sinonjs/sinon/issues/2412)) (Julian Grinblat) > - Fix bundling of cjs module > > - Run prettier *Released by [Carl-Erik Kopseng](https://togithub.com/fatso83) on 2021-11-04.* #### 12.0.0 ### [`v12.0.0`](https://togithub.com/sinonjs/sinon/compare/v11.1.2...v12.0.0) [Compare Source](https://togithub.com/sinonjs/sinon/compare/v11.1.2...v12.0.0)
--- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c62f6381..e5c5c4c1 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "nock": "^13.0.0", "null-loader": "^4.0.0", "puppeteer": "^11.0.0", - "sinon": "^11.0.0", + "sinon": "^12.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "^3.8.3", From db2f1eab24e12db4fb2595a466d61c3d2f52de56 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 8 Nov 2021 11:03:33 -0500 Subject: [PATCH 306/662] test: retry flaky pack/install test (#1316) Retry the pack and install test, which has several moving parts and fails once in a blue moon. Refactored such that tests are now independent, i.e., npm pack and webpack can be run independently. Fixes #1315 --- system-test/test.kitchen.ts | 41 +++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 9a298958..49d4b86d 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it, before, after} from 'mocha'; +import {describe, it, afterEach} from 'mocha'; import * as execa from 'execa'; import * as fs from 'fs'; import * as mv from 'mv'; @@ -25,40 +25,51 @@ import {promisify} from 'util'; const mvp = promisify(mv) as {} as (...args: string[]) => Promise; const ncpp = promisify(ncp); const keep = !!process.env.GALN_KEEP_TEMPDIRS; -const stagingDir = tmp.dirSync({keep, unsafeCleanup: true}); -const stagingPath = stagingDir.name; // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../package.json'); +let stagingDir: tmp.DirResult; +let stagingPath: string; +async function packAndInstall() { + stagingDir = tmp.dirSync({keep, unsafeCleanup: true}); + stagingPath = stagingDir.name; + await execa('npm', ['pack'], {stdio: 'inherit'}); + const tarball = `${pkg.name}-${pkg.version}.tgz`; + // stagingPath can be on another filesystem so fs.rename() will fail + // with EXDEV, hence we use `mv` module here. + await mvp(tarball, `${stagingPath}/google-auth-library.tgz`); + await ncpp('system-test/fixtures/kitchen', `${stagingPath}/`); + await execa('npm', ['install'], {cwd: `${stagingPath}/`, stdio: 'inherit'}); +} + describe('pack and install', () => { /** * Create a staging directory with temp fixtures used to test on a fresh * application. */ - before('should be able to use the d.ts', async function () { + it('should be able to use the d.ts', async function () { + // npm, once in a blue moon, fails during pack process. If this happens, + // we should be safe to retry. + //this.retries(3); this.timeout(40000); - console.log(`${__filename} staging area: ${stagingPath}`); - await execa('npm', ['pack'], {stdio: 'inherit'}); - const tarball = `${pkg.name}-${pkg.version}.tgz`; - // stagingPath can be on another filesystem so fs.rename() will fail - // with EXDEV, hence we use `mv` module here. - await mvp(tarball, `${stagingPath}/google-auth-library.tgz`); - await ncpp('system-test/fixtures/kitchen', `${stagingPath}/`); - await execa('npm', ['install'], {cwd: `${stagingPath}/`, stdio: 'inherit'}); + await packAndInstall(); }); - it('should be able to webpack the library', async () => { + it('should be able to webpack the library', async function () { + this.retries(3); + this.timeout(40000); + await packAndInstall(); // we expect npm install is executed in the before hook await execa('npx', ['webpack'], {cwd: `${stagingPath}/`, stdio: 'inherit'}); const bundle = path.join(stagingPath, 'dist', 'bundle.min.js'); const stat = fs.statSync(bundle); assert(stat.size < 256 * 1024); - }).timeout(20000); + }); /** * CLEAN UP - remove the staging directory when done. */ - after('cleanup staging', () => { + afterEach('cleanup staging', () => { if (!keep) { stagingDir.removeCallback(); } From 30bbd690f1b0e6dd4d233e5410b43d73ca05385a Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 8 Nov 2021 12:35:02 -0500 Subject: [PATCH 307/662] test: remove commented out retries (#1317) --- system-test/test.kitchen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 49d4b86d..f9698772 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -50,7 +50,7 @@ describe('pack and install', () => { it('should be able to use the d.ts', async function () { // npm, once in a blue moon, fails during pack process. If this happens, // we should be safe to retry. - //this.retries(3); + this.retries(3); this.timeout(40000); await packAndInstall(); }); From 5ed910513451c82e2551777a3e2212964799ef8e Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Mon, 8 Nov 2021 10:53:22 -0800 Subject: [PATCH 308/662] chore(serverless): add missing var to snippet block (#1212) --- samples/idtokens-serverless.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index 9b02821a..f50f0460 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -22,6 +22,12 @@ function main( url = 'https://service-1234-uc.a.run.app', targetAudience = null ) { + if (!targetAudience) { + // Use the target service's hostname as the target audience for requests. + // (For example: https://my-cloud-run-service.run.app) + const {URL} = require('url'); + targetAudience = new URL(url).origin; + } // [START google_auth_idtoken_serverless] // [START cloudrun_service_to_service_auth] // [START run_service_to_service_auth] @@ -29,17 +35,15 @@ function main( /** * TODO(developer): Uncomment these variables before running the sample. */ - // const url = 'https://TARGET_URL'; - // let targetAudience = null; + // Example: https://my-cloud-run-service.run.app/books/delete/12345 + // const url = 'https://TARGET_HOSTNAME/TARGET_URL'; + + // Example: https://my-cloud-run-service.run.app/ + // const targetAudience = 'https://TARGET_HOSTNAME/'; const {GoogleAuth} = require('google-auth-library'); const auth = new GoogleAuth(); async function request() { - if (!targetAudience) { - // Use the request URL hostname as the target audience for requests. - const {URL} = require('url'); - targetAudience = new URL(url); - } console.info(`request ${url} with target audience ${targetAudience}`); const client = await auth.getIdTokenClient(targetAudience); const res = await client.request({url}); From 110ddc245b768888a88d8c7211f0b0391326cc10 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 29 Nov 2021 18:33:58 +0100 Subject: [PATCH 309/662] fix(deps): update dependency puppeteer to v12 (#1325) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e5c5c4c1..4255cc1a 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^11.0.0", + "puppeteer": "^12.0.0", "sinon": "^12.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 3532d06f..76fc50f7 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^7.0.0", - "puppeteer": "^11.0.0" + "puppeteer": "^12.0.0" } } From 05529155b40f4ec41c97c5ae7fca635d9c90653f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 3 Dec 2021 14:25:56 -0800 Subject: [PATCH 310/662] chore: release 7.10.3 (#1326) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d074c16..43034c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.10.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.2...v7.10.3) (2021-11-29) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v12 ([#1325](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1325)) ([110ddc2](https://www.github.com/googleapis/google-auth-library-nodejs/commit/110ddc245b768888a88d8c7211f0b0391326cc10)) + ### [7.10.2](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.1...v7.10.2) (2021-11-03) diff --git a/package.json b/package.json index 4255cc1a..4970bcb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.10.2", + "version": "7.10.3", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 5f908d04..79cf9725 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.10.2", + "google-auth-library": "^7.10.3", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 9c3ceedfb38b819ab4b441b8160c37eea74dc472 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 9 Dec 2021 22:40:24 +0000 Subject: [PATCH 311/662] build: add generated samples to .eslintignore (#1333) --- .eslintignore | 1 + .github/.OwlBot.lock.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index 9340ad9b..ea5b04ae 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,4 @@ test/fixtures build/ docs/ protos/ +samples/generated/ diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 8d0a479d..8a63b10a 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:bbb8dd6576ac58830a07fc17e9511ae898be44f2219d3344449b125df9854441 + digest: sha256:ba3f2990fefe465f89834e4c46f847ddb141afa54daa6a1d462928fa679ed143 From e05548dfb431638d618aa8b846d0944541387033 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 13 Dec 2021 18:42:26 +0100 Subject: [PATCH 312/662] fix(deps): update dependency puppeteer to v13 (#1334) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4970bcb3..cc01dd74 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^12.0.0", + "puppeteer": "^13.0.0", "sinon": "^12.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 76fc50f7..ce54cfb0 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^7.0.0", - "puppeteer": "^12.0.0" + "puppeteer": "^13.0.0" } } From e91472a941c2695b8f702076a8f70b621dde528f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 13 Dec 2021 17:48:12 +0000 Subject: [PATCH 313/662] chore: release 7.10.4 (#1335) :robot: I have created a release \*beep\* \*boop\* --- ### [7.10.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.3...v7.10.4) (2021-12-13) ### Bug Fixes * **deps:** update dependency puppeteer to v13 ([#1334](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1334)) ([e05548d](https://www.github.com/googleapis/google-auth-library-nodejs/commit/e05548dfb431638d618aa8b846d0944541387033)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43034c3d..ede74861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.10.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.3...v7.10.4) (2021-12-13) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v13 ([#1334](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1334)) ([e05548d](https://www.github.com/googleapis/google-auth-library-nodejs/commit/e05548dfb431638d618aa8b846d0944541387033)) + ### [7.10.3](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.2...v7.10.3) (2021-11-29) diff --git a/package.json b/package.json index cc01dd74..7516d8c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.10.3", + "version": "7.10.4", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 79cf9725..ba2e2b14 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.10.3", + "google-auth-library": "^7.10.4", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 454911637eca6a1272a729b2b2dfcf690c53fe29 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Wed, 15 Dec 2021 11:58:20 -0800 Subject: [PATCH 314/662] feat: adds README, samples and integration tests for downscoping with CAB (#1311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add description, samples and integration tests for CAB * fix lints * fix copyright format and import * add storage dependency * try on header fix * fix some comments * update readme * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * change yaml and fixes comments * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * revertbucketName and objectName * revert logic in try blocks * tweak child exec process * fix lint * set bucket name and object name environment variable Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> Co-authored-by: Owl Bot --- .readme-partials.yaml | 113 +++++++++++++++++ README.md | 115 ++++++++++++++++++ samples/README.md | 18 +++ samples/downscopedclient.js | 106 ++++++++++++++++ samples/package.json | 3 +- samples/scripts/downscoping-with-cab-setup.js | 90 ++++++++++++++ samples/test/downscoping-with-cab.test.js | 65 ++++++++++ 7 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 samples/downscopedclient.js create mode 100644 samples/scripts/downscoping-with-cab-setup.js create mode 100644 samples/test/downscoping-with-cab.test.js diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 6f47fb10..f1bfe053 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -9,6 +9,7 @@ body: |- - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). - [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account. + - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. @@ -676,3 +677,115 @@ body: |- main(); ``` + ## Downscoped Client + + [Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials) is used to restrict the Identity and Access Management (IAM) permissions that a short-lived credential can use. + + The `DownscopedClient` class can be used to produce a downscoped access token from a + `CredentialAccessBoundary` and a source credential. The Credential Access Boundary specifies which resources the newly created credential can access, as well as an upper bound on the permissions that are available on each resource. Using downscoped credentials ensures tokens in flight always have the least privileges, e.g. Principle of Least Privilege. + + > Notice: Only Cloud Storage supports Credential Access Boundaries for now. + + ### Sample Usage + There are two entities needed to generate and use credentials generated from + Downscoped Client with Credential Access Boundaries. + + - Token broker: This is the entity with elevated permissions. This entity has the permissions needed to generate downscoped tokens. The common pattern of usage is to have a token broker with elevated access generate these downscoped credentials from higher access source credentials and pass the downscoped short-lived access tokens to a token consumer via some secure authenticated channel for limited access to Google Cloud Storage resources. + + ``` js + const {GoogleAuth, DownscopedClient} = require('google-auth-library'); + // Define CAB rules which will restrict the downscoped token to have readonly + // access to objects starting with "customer-a" in bucket "bucket_name". + const cabRules = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: `//storage.googleapis.com/projects/_/buckets/bucket_name`, + availablePermissions: ['inRole:roles/storage.objectViewer'], + availabilityCondition: { + expression: + `resource.name.startsWith('projects/_/buckets/` + + `bucket_name/objects/customer-a)` + } + }, + ], + }, + }; + + // This will use ADC to get the credentials used for the downscoped client. + const googleAuth = new GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/cloud-platform'] + }); + + // Obtain an authenticated client via ADC. + const client = await googleAuth.getClient(); + + // Use the client to create a DownscopedClient. + const cabClient = new DownscopedClient(client, cab); + + // Refresh the tokens. + const refreshedAccessToken = await cabClient.getAccessToken(); + + // This will need to be passed to the token consumer. + access_token = refreshedAccessToken.token; + expiry_date = refreshedAccessToken.expirationTime; + ``` + + A token broker can be set up on a server in a private network. Various workloads + (token consumers) in the same network will send authenticated requests to that broker for downscoped tokens to access or modify specific google cloud storage buckets. + + The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer. + + - Token consumer: This is the consumer of the downscoped tokens. This entity does not have the direct ability to generate access tokens and instead relies on the token broker to provide it with downscoped tokens to run operations on GCS buckets. It is assumed that the downscoped token consumer may have its own mechanism to authenticate itself with the token broker. + + ``` js + const {OAuth2Client} = require('google-auth-library'); + const {Storage} = require('@google-cloud/storage'); + + // Create the OAuth credentials (the consumer). + const oauth2Client = new OAuth2Client(); + // We are defining a refresh handler instead of a one-time access + // token/expiry pair. + // This will allow the consumer to obtain new downscoped tokens on + // demand every time a token is expired, without any additional code + // changes. + oauth2Client.refreshHandler = async () => { + // The common pattern of usage is to have a token broker pass the + // downscoped short-lived access tokens to a token consumer via some + // secure authenticated channel. + const refreshedAccessToken = await cabClient.getAccessToken(); + return { + access_token: refreshedAccessToken.token, + expiry_date: refreshedAccessToken.expirationTime, + } + }; + + // Use the consumer client to define storageOptions and create a GCS object. + const storageOptions = { + projectId: 'my_project_id', + authClient: { + sign: () => Promise.reject('unsupported'), + getCredentials: () => Promise.reject(), + request: (opts, callback) => { + return oauth2Client.request(opts, callback); + }, + authorizeRequest: async (opts) => { + opts = opts || {}; + const url = opts.url || opts.uri; + const headers = await oauth2Client.getRequestHeaders(url); + opts.headers = Object.assign(opts.headers || {}, headers); + return opts; + }, + }, + }; + + const storage = new Storage(storageOptions); + + const downloadFile = await storage + .bucket('bucket_name') + .file('customer-a-data.txt') + .download(); + console.log(downloadFile.toString('utf8')); + + main().catch(console.error); + ``` \ No newline at end of file diff --git a/README.md b/README.md index f5804c37..435780af 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ This library provides a variety of ways to authenticate to your Google services. - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). - [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account. +- [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. @@ -721,6 +722,119 @@ async function main() { main(); ``` +## Downscoped Client + +[Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials) is used to restrict the Identity and Access Management (IAM) permissions that a short-lived credential can use. + +The `DownscopedClient` class can be used to produce a downscoped access token from a +`CredentialAccessBoundary` and a source credential. The Credential Access Boundary specifies which resources the newly created credential can access, as well as an upper bound on the permissions that are available on each resource. Using downscoped credentials ensures tokens in flight always have the least privileges, e.g. Principle of Least Privilege. + +> Notice: Only Cloud Storage supports Credential Access Boundaries for now. + +### Sample Usage +There are two entities needed to generate and use credentials generated from +Downscoped Client with Credential Access Boundaries. + +- Token broker: This is the entity with elevated permissions. This entity has the permissions needed to generate downscoped tokens. The common pattern of usage is to have a token broker with elevated access generate these downscoped credentials from higher access source credentials and pass the downscoped short-lived access tokens to a token consumer via some secure authenticated channel for limited access to Google Cloud Storage resources. + +``` js +const {GoogleAuth, DownscopedClient} = require('google-auth-library'); +// Define CAB rules which will restrict the downscoped token to have readonly +// access to objects starting with "customer-a" in bucket "bucket_name". +const cabRules = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: `//storage.googleapis.com/projects/_/buckets/bucket_name`, + availablePermissions: ['inRole:roles/storage.objectViewer'], + availabilityCondition: { + expression: + `resource.name.startsWith('projects/_/buckets/` + + `bucket_name/objects/customer-a)` + } + }, + ], + }, +}; + +// This will use ADC to get the credentials used for the downscoped client. +const googleAuth = new GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/cloud-platform'] +}); + +// Obtain an authenticated client via ADC. +const client = await googleAuth.getClient(); + +// Use the client to create a DownscopedClient. +const cabClient = new DownscopedClient(client, cab); + +// Refresh the tokens. +const refreshedAccessToken = await cabClient.getAccessToken(); + +// This will need to be passed to the token consumer. +access_token = refreshedAccessToken.token; +expiry_date = refreshedAccessToken.expirationTime; +``` + +A token broker can be set up on a server in a private network. Various workloads +(token consumers) in the same network will send authenticated requests to that broker for downscoped tokens to access or modify specific google cloud storage buckets. + +The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer. + +- Token consumer: This is the consumer of the downscoped tokens. This entity does not have the direct ability to generate access tokens and instead relies on the token broker to provide it with downscoped tokens to run operations on GCS buckets. It is assumed that the downscoped token consumer may have its own mechanism to authenticate itself with the token broker. + +``` js +const {OAuth2Client} = require('google-auth-library'); +const {Storage} = require('@google-cloud/storage'); + +// Create the OAuth credentials (the consumer). +const oauth2Client = new OAuth2Client(); +// We are defining a refresh handler instead of a one-time access +// token/expiry pair. +// This will allow the consumer to obtain new downscoped tokens on +// demand every time a token is expired, without any additional code +// changes. +oauth2Client.refreshHandler = async () => { + // The common pattern of usage is to have a token broker pass the + // downscoped short-lived access tokens to a token consumer via some + // secure authenticated channel. + const refreshedAccessToken = await cabClient.getAccessToken(); + return { + access_token: refreshedAccessToken.token, + expiry_date: refreshedAccessToken.expirationTime, + } +}; + +// Use the consumer client to define storageOptions and create a GCS object. +const storageOptions = { + projectId: 'my_project_id', + authClient: { + sign: () => Promise.reject('unsupported'), + getCredentials: () => Promise.reject(), + request: (opts, callback) => { + return oauth2Client.request(opts, callback); + }, + authorizeRequest: async (opts) => { + opts = opts || {}; + const url = opts.url || opts.uri; + const headers = await oauth2Client.getRequestHeaders(url); + opts.headers = Object.assign(opts.headers || {}, headers); + return opts; + }, + }, +}; + +const storage = new Storage(storageOptions); + +const downloadFile = await storage + .bucket('bucket_name') + .file('customer-a-data.txt') + .download(); +console.log(downloadFile.toString('utf8')); + +main().catch(console.error); +``` + ## Samples @@ -731,6 +845,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar | Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/adc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/adc.js,samples/README.md) | | Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) | | Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/credentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/credentials.js,samples/README.md) | +| Downscopedclient | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/downscopedclient.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/downscopedclient.js,samples/README.md) | | Headers | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/headers.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/headers.js,samples/README.md) | | ID Tokens for Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) | | ID Tokens for Serverless | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) | diff --git a/samples/README.md b/samples/README.md index 0729eae1..123cd26c 100644 --- a/samples/README.md +++ b/samples/README.md @@ -15,6 +15,7 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra * [Adc](#adc) * [Compute](#compute) * [Credentials](#credentials) + * [Downscopedclient](#downscopedclient) * [Headers](#headers) * [ID Tokens for Identity-Aware Proxy (IAP)](#id-tokens-for-identity-aware-proxy-iap) * [ID Tokens for Serverless](#id-tokens-for-serverless) @@ -93,6 +94,23 @@ __Usage:__ +### Downscopedclient + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/downscopedclient.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/downscopedclient.js,samples/README.md) + +__Usage:__ + + +`node samples/downscopedclient.js` + + +----- + + + + ### Headers View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/headers.js). diff --git a/samples/downscopedclient.js b/samples/downscopedclient.js new file mode 100644 index 00000000..edd10ea5 --- /dev/null +++ b/samples/downscopedclient.js @@ -0,0 +1,106 @@ +// Copyright 2021 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. + +'use strict'; + +/** + * Imports the Google Auth and Google Cloud libraries. + */ +const { + OAuth2Client, + GoogleAuth, + DownscopedClient, +} = require('google-auth-library'); +const {Storage} = require('@google-cloud/storage'); + +/** + * The following sample demonstrates how to initialize a DownscopedClient using + * a credential access boundary and a client obtained via ADC. The + * DownscopedClient is used to create downscoped tokens which can be consumed + * via the OAuth2Client. A refresh handler is used to obtain new downscoped + * tokens seamlessly when they expire. Then the oauth2Client is used to define + * a cloud storage object and call GCS APIs to access specified object and + * print the contents. + */ +async function main() { + const bucketName = process.env.BUCKET_NAME; + const objectName = process.env.OBJECT_NAME; + // Defines a credential access boundary that grants objectViewer access in + // the specified bucket. + const cab = { + accessBoundary: { + accessBoundaryRules: [ + { + availableResource: `//storage.googleapis.com/projects/_/buckets/${bucketName}`, + availablePermissions: ['inRole:roles/storage.objectViewer'], + availabilityCondition: { + expression: + "resource.name.startsWith('projects/_/buckets/" + + `${bucketName}/objects/${objectName}')`, + }, + }, + ], + }, + }; + + const oauth2Client = new OAuth2Client(); + const googleAuth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }); + const projectId = await googleAuth.getProjectId(); + // Obtain an authenticated client via ADC. + const client = await googleAuth.getClient(); + // Use the client to generate a DownscopedClient. + const cabClient = new DownscopedClient(client, cab); + // Define a refreshHandler that will be used to refresh the downscoped token + // when it expires. + oauth2Client.refreshHandler = async () => { + const refreshedAccessToken = await cabClient.getAccessToken(); + return { + access_token: refreshedAccessToken.token, + expiry_date: refreshedAccessToken.expirationTime, + }; + }; + + const storageOptions = { + projectId, + authClient: { + getCredentials: async () => { + Promise.reject(); + }, + request: opts => { + return oauth2Client.request(opts); + }, + sign: () => { + Promise.reject('unsupported'); + }, + authorizeRequest: async opts => { + opts = opts || {}; + const url = opts.url || opts.uri; + const headers = await oauth2Client.getRequestHeaders(url); + opts.headers = Object.assign(opts.headers || {}, headers); + return opts; + }, + }, + }; + + const storage = new Storage(storageOptions); + const downloadFile = await storage + .bucket(bucketName) + .file(objectName) + .download(); + console.log(downloadFile.toString('utf8')); +} + +main().catch(console.error); diff --git a/samples/package.json b/samples/package.json index ba2e2b14..7588ef7f 100644 --- a/samples/package.json +++ b/samples/package.json @@ -5,7 +5,7 @@ "*.js" ], "scripts": { - "setup": "node scripts/externalclient-setup.js", + "setup": "node scripts/*.js", "test": "mocha --timeout 60000" }, "engines": { @@ -13,6 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", "google-auth-library": "^7.10.4", "node-fetch": "^2.3.0", diff --git a/samples/scripts/downscoping-with-cab-setup.js b/samples/scripts/downscoping-with-cab-setup.js new file mode 100644 index 00000000..a0f37700 --- /dev/null +++ b/samples/scripts/downscoping-with-cab-setup.js @@ -0,0 +1,90 @@ +// Copyright 2021 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. + +// This script is used to generate the project configurations needed to +// end-to-end test Downscoping with Credential Access Boundaries in the Auth +// library. + +// In order to run this script, the GOOGLE_APPLICATION_CREDENTIALS environment +// variable needs to be set to point to a service account key file. +// +// GCP project changes: +// -------------------- +// The following IAM role need to be set on the service account: +// 1. Storage Admin (needed to create bucket and object). + +// This script needs to be run once. It will do the following: +// 1. Generates a random ID for bucketName and objectName. +// 2. Creates a GCS bucket in the specified project defined in GOOGLE_APPLICATION_CREDENTIALS. +// 3. Creates two object in the bucket created in the last step. +// 4. Prints out the identifiers (bucketName, first objectName, second objectName) +// to be used in the accompanying tests. +// +// The same service account used for this setup script should be used for +// the integration tests. +// +// It is safe to run the setup script again. A new bucket is created along with +// new objects. If run multiple times, it is advisable to delete +// unused buckets. + +const {Storage} = require('@google-cloud/storage'); +const fs = require('fs'); +const {promisify} = require('util'); + +const readFile = promisify(fs.readFile); +const CONTENT = 'first'; + +/** + * Generates a random string of the specified length, optionally using the + * specified alphabet. + * + * @param {number} length The length of the string to generate. + * @return {string} A random string of the provided length. + */ +function generateRandomString(length) { + const chars = []; + const allowedChars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < length; i++) { + chars.push( + allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)) + ); + } + return chars.join(''); +} + +async function main() { + const keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS; + if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) { + throw new Error('No GOOGLE_APPLICATION_CREDENTIALS env var is available'); + } + const keys = JSON.parse(await readFile(keyFile, 'utf8')); + + const suffix = generateRandomString(10); + const bucketName = `cab-int-bucket-${suffix}`; + const objectName = `cab-first-${suffix}.txt`; + const projectId = keys.project_id; + const storage = new Storage(projectId); + + try { + await storage.createBucket(bucketName); + await storage.bucket(bucketName).file(objectName).save(CONTENT); + } catch (error) { + console.log(error.message); + } + + console.log('bucket name: ' + bucketName); + console.log('object name: ' + objectName); +} + +main().catch(console.error); diff --git a/samples/test/downscoping-with-cab.test.js b/samples/test/downscoping-with-cab.test.js new file mode 100644 index 00000000..b8bc6dee --- /dev/null +++ b/samples/test/downscoping-with-cab.test.js @@ -0,0 +1,65 @@ +// Copyright 2021 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. + +// Prerequisites: +// Make sure to run the setup in samples/scripts/downscoping-with-cab-setup.js +// and copy the logged constant strings (bucketName, objectName1 and +// objectName2) into this file before running this test suite. +// Once that is done, this test can be run indefinitely. +// +// The only requirement for this test suite to run is to set the environment +// variable GOOGLE_APPLICATION_CREDENTIALS to point to the same service account +// keys used in the setup script. + +const cp = require('child_process'); +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const {promisify} = require('util'); + +const exec = promisify(cp.exec); +// Copy values from the output of samples/scripts/downscoping-with-cab-setup.js. +// GCS bucket name. +const bucketName = 'cab-int-bucket-z2zsauf4sj'; +// GCS object name. +const objectName = 'cab-first-z2zsauf4sj.txt'; + +/** + * Runs the provided command using asynchronous child_process.exec. + * Unlike execSync, this works with another local HTTP server running in the + * background. + * @param {string} cmd The actual command string to run. + * @param {*} opts The optional parameters for child_process.exec. + * @return {Promise} A promise that resolves with a string + * corresponding with the terminal output. + */ +const execAsync = async (cmd, opts) => { + const {stdout, stderr} = await exec(cmd, opts); + return stdout + stderr; +}; + +describe('samples for downscoping with cab', () => { + it('should have access to the object specified in the cab rule', async () => { + const output = await execAsync(`${process.execPath} downscopedclient`, { + env: { + ...process.env, + // GCS bucket name environment variable. + BUCKET_NAME: bucketName, + // GCS object name environment variable. + OBJECT_NAME: objectName, + }, + }); + // Confirm expected script output. + assert.match(output, /first/); + }); +}); From 7b6c04185088b028af31a22c88ad5d78e9f6f2fb Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:04:11 +0000 Subject: [PATCH 315/662] chore: release 7.11.0 (#1336) :robot: I have created a release \*beep\* \*boop\* --- ## [7.11.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.4...v7.11.0) (2021-12-15) ### Features * adds README, samples and integration tests for downscoping with CAB ([#1311](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1311)) ([4549116](https://www.github.com/googleapis/google-auth-library-nodejs/commit/454911637eca6a1272a729b2b2dfcf690c53fe29)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede74861..73150108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.11.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.4...v7.11.0) (2021-12-15) + + +### Features + +* adds README, samples and integration tests for downscoping with CAB ([#1311](https://www.github.com/googleapis/google-auth-library-nodejs/issues/1311)) ([4549116](https://www.github.com/googleapis/google-auth-library-nodejs/commit/454911637eca6a1272a729b2b2dfcf690c53fe29)) + ### [7.10.4](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.3...v7.10.4) (2021-12-13) diff --git a/package.json b/package.json index 7516d8c0..eb4ff9c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.10.4", + "version": "7.11.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 7588ef7f..60f4a0da 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.10.4", + "google-auth-library": "^7.11.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From a5ac4362e6198a332daacbe90390ff1765faa0f7 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Mon, 20 Dec 2021 12:25:18 -0800 Subject: [PATCH 316/662] samples: audience for Cloud Functions needs path (#1340) Cloud Functions audience needs to include the path to the function. --- samples/idtokens-serverless.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index f50f0460..5d2dc48b 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -38,7 +38,8 @@ function main( // Example: https://my-cloud-run-service.run.app/books/delete/12345 // const url = 'https://TARGET_HOSTNAME/TARGET_URL'; - // Example: https://my-cloud-run-service.run.app/ + // Example (Cloud Run): https://my-cloud-run-service.run.app/ + // Example (Cloud Functions): https://project-region-projectid.cloudfunctions.net/myFunction // const targetAudience = 'https://TARGET_HOSTNAME/'; const {GoogleAuth} = require('google-auth-library'); const auth = new GoogleAuth(); From d2d80e568d634c94e56c7ace27b76f43a16b5962 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 29 Dec 2021 19:50:35 +0000 Subject: [PATCH 317/662] docs(node): support "stable"/"preview" release level (#1312) (#1342) --- .github/.OwlBot.lock.yaml | 2 +- README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 8a63b10a..1b6a76cc 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:ba3f2990fefe465f89834e4c46f847ddb141afa54daa6a1d462928fa679ed143 + digest: sha256:5ed10ba99cd1ea8c3a0f29b4c53e8a2723a101952705baed6b61783111c64c1c diff --git a/README.md b/README.md index 435780af..75c11afc 100644 --- a/README.md +++ b/README.md @@ -899,6 +899,8 @@ are addressed with the highest priority. + + More Information: [Google Cloud Platform Launch Stages][launch_stages] [launch_stages]: https://cloud.google.com/terms/launch-stages From 7862a50120a8b53e538eac68d47c39e217a0279c Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Dec 2021 11:01:10 -0500 Subject: [PATCH 318/662] chore: add api_shortname and library_type to repo metadata (#1341) --- .repo-metadata.json | 5 +++-- README.md | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.repo-metadata.json b/.repo-metadata.json index 106512f4..085ffd33 100644 --- a/.repo-metadata.json +++ b/.repo-metadata.json @@ -3,9 +3,10 @@ "name_pretty": "Google Auth Library", "product_documentation": "https://cloud.google.com/docs/authentication/", "client_documentation": "https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest", - "release_level": "ga", + "release_level": "stable", "language": "nodejs", "repo": "googleapis/google-auth-library-nodejs", "distribution_name": "google-auth-library", - "codeowner_team": "@googleapis/nodejs-auth" + "codeowner_team": "@googleapis/nodejs-auth", + "library_type": "AUTH" } diff --git a/README.md b/README.md index 75c11afc..642330dc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # [Google Auth Library: Node.js Client](https://github.com/googleapis/google-auth-library-nodejs) -[![release level](https://img.shields.io/badge/release%20level-general%20availability%20%28GA%29-brightgreen.svg?style=flat)](https://cloud.google.com/terms/launch-stages) + [![npm version](https://img.shields.io/npm/v/google-auth-library.svg)](https://www.npmjs.org/package/google-auth-library) [![codecov](https://img.shields.io/codecov/c/github/googleapis/google-auth-library-nodejs/main.svg?style=flat)](https://codecov.io/gh/googleapis/google-auth-library-nodejs) @@ -889,10 +889,10 @@ _Legacy Node.js versions are supported as a best effort:_ This library follows [Semantic Versioning](http://semver.org/). -This library is considered to be **General Availability (GA)**. This means it -is stable; the code surface will not change in backwards-incompatible ways + +This library is considered to be **stable**. The code surface will not change in backwards-incompatible ways unless absolutely necessary (e.g. because of critical security issues) or with -an extensive deprecation period. Issues and requests against **GA** libraries +an extensive deprecation period. Issues and requests against **stable** libraries are addressed with the highest priority. @@ -900,7 +900,6 @@ are addressed with the highest priority. - More Information: [Google Cloud Platform Launch Stages][launch_stages] [launch_stages]: https://cloud.google.com/terms/launch-stages From 580c5bd1984bbe0de1ec03b5f087303c9a3a78c3 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 30 Dec 2021 23:18:30 +0000 Subject: [PATCH 319/662] docs(badges): tweak badge to use new preview/stable language (#1314) (#1344) --- .github/.OwlBot.lock.yaml | 2 +- README.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 1b6a76cc..497345b8 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:5ed10ba99cd1ea8c3a0f29b4c53e8a2723a101952705baed6b61783111c64c1c + digest: sha256:f092066de33d4a2a13ab13c8fa9dcb4f6b96fa1fb7d391bf19cd0c4921d997c0 diff --git a/README.md b/README.md index 642330dc..968d6a58 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ # [Google Auth Library: Node.js Client](https://github.com/googleapis/google-auth-library-nodejs) - +[![release level](https://img.shields.io/badge/release%20level-stable-brightgreen.svg?style=flat)](https://cloud.google.com/terms/launch-stages) [![npm version](https://img.shields.io/npm/v/google-auth-library.svg)](https://www.npmjs.org/package/google-auth-library) -[![codecov](https://img.shields.io/codecov/c/github/googleapis/google-auth-library-nodejs/main.svg?style=flat)](https://codecov.io/gh/googleapis/google-auth-library-nodejs) From 58c53b44113a7211884d49dac1683032a5ce681e Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 21:04:45 -0500 Subject: [PATCH 320/662] test(nodejs): remove 15 add 16 (#1322) (#1346) Source-Link: https://github.com/googleapis/synthtool/commit/6981da4f29c0ae3dd783d58f1be5ab222d6a5642 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:3563b6b264989c4f5aa31a3682e4df36c95756cfef275d3201508947cbfc511e Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 2 +- .github/workflows/ci.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 497345b8..6831fd8e 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:f092066de33d4a2a13ab13c8fa9dcb4f6b96fa1fb7d391bf19cd0c4921d997c0 + digest: sha256:3563b6b264989c4f5aa31a3682e4df36c95756cfef275d3201508947cbfc511e diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dd397054..a9517884 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 15] + node: [10, 12, 14, 16] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 From 097d3283a9c34da9ec9734079730e22bbd7daf6e Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 20:08:33 +0000 Subject: [PATCH 321/662] chore: update github issue templates (#1085) (#1347) --- .github/.OwlBot.lock.yaml | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/config.yml | 4 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 12 ++++++++++++ 5 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 6831fd8e..cbbb1758 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:3563b6b264989c4f5aa31a3682e4df36c95756cfef275d3201508947cbfc511e + digest: sha256:2d850512335d7adca3a4b08e02f8e63192978aea88c042dacb3e382aa996ae7c diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 60125d9d..85050fe9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Create a report to help us improve - +labels: 'type: bug, priority: p2' --- Thanks for stopping by to let us know something could be better! diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..603b9013 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Google Cloud Support + url: https://cloud.google.com/support/ + about: If you have a support contract with Google, please use the Google Cloud Support portal. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6365857f..b0327dfa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea for this library - +labels: 'type: feature request, priority: p3' --- Thanks for stopping by to let us know something could be better! diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..97323113 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,12 @@ +--- +name: Question +about: Ask a question +labels: 'type: question, priority: p3' +--- + +Thanks for stopping by to ask us a question! Please make sure to include: +- What you're trying to do +- What code you've already tried +- Any error messages you're getting + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. From 34ca2a0797a3ee8ba8dea90bd88716599185e4b0 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 19 Jan 2022 17:46:32 +0000 Subject: [PATCH 322/662] build(node): switch back to keystore for publication (#1328) (#1348) --- .github/.OwlBot.lock.yaml | 2 +- .kokoro/publish.sh | 2 +- .kokoro/release/publish.cfg | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index cbbb1758..2c37ca7a 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:2d850512335d7adca3a4b08e02f8e63192978aea88c042dacb3e382aa996ae7c + digest: sha256:89c5b2f3decec8ad64febbebea671076c119d1ab43700da380846a315600de8a diff --git a/.kokoro/publish.sh b/.kokoro/publish.sh index 4db6bf1c..77a5defb 100755 --- a/.kokoro/publish.sh +++ b/.kokoro/publish.sh @@ -24,7 +24,7 @@ python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source / cd $(dirname $0)/.. -NPM_TOKEN=$(cat $KOKORO_GFILE_DIR/secret_manager/npm_publish_token) +NPM_TOKEN=$(cat $KOKORO_KEYSTORE_DIR/73713_google-cloud-npm-token-1) echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_TOKEN}" > ~/.npmrc npm install diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index 3ca302f2..90b0bc09 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -7,9 +7,18 @@ before_action { } } +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "google-cloud-npm-token-1" + } + } +} + env_vars: { key: "SECRET_MANAGER_KEYS" - value: "npm_publish_token,releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" + value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" } # Download trampoline resources. From ac84aa889991172c99e3125cd4806ca845ed8eed Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 26 Jan 2022 10:32:12 +0100 Subject: [PATCH 323/662] chore(deps): update actions/setup-node action to v2 (#1353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-node](https://togithub.com/actions/setup-node) | action | major | `v1` -> `v2` | --- ### Release Notes
actions/setup-node ### [`v2`](https://togithub.com/actions/setup-node/compare/v1...v2) [Compare Source](https://togithub.com/actions/setup-node/compare/v1...v2)
--- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). From c23ae13863f38d43d575b8a930fa701720fcdc88 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 31 Jan 2022 23:32:34 +0100 Subject: [PATCH 324/662] chore(deps): update dependency sinon to v13 (#1354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [sinon](https://sinonjs.org/) ([source](https://togithub.com/sinonjs/sinon)) | [`^12.0.0` -> `^13.0.0`](https://renovatebot.com/diffs/npm/sinon/12.0.1/13.0.0) | [![age](https://badges.renovateapi.com/packages/npm/sinon/13.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/sinon/13.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/sinon/13.0.0/compatibility-slim/12.0.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/sinon/13.0.0/confidence-slim/12.0.1)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
sinonjs/sinon ### [`v13.0.0`](https://togithub.com/sinonjs/sinon/blob/HEAD/CHANGES.md#​1300) [Compare Source](https://togithub.com/sinonjs/sinon/compare/v12.0.1...v13.0.0) - [`cf3d6c0c`](https://togithub.com/sinonjs/sinon/commit/cf3d6c0cd9689c0ee673b3daa8bf9abd70304392) Upgrade packages ([#​2431](https://togithub.com/sinonjs/sinon/issues/2431)) (Carl-Erik Kopseng) > - Update all @​sinonjs/ packages > > - Upgrade to fake-timers 9 > > - chore: ensure always using latest LTS release - [`41710467`](https://togithub.com/sinonjs/sinon/commit/417104670d575e96a1b645ea40ce763afa76fb1b) Adjust deploy scripts to archive old releases in a separate branch, move existing releases out of master ([#​2426](https://togithub.com/sinonjs/sinon/issues/2426)) (Joel Bradshaw) > Co-authored-by: Carl-Erik Kopseng - [`c80a7266`](https://togithub.com/sinonjs/sinon/commit/c80a72660e89d88b08275eff1028ecb9e26fd8e9) Bump node-fetch from 2.6.1 to 2.6.7 ([#​2430](https://togithub.com/sinonjs/sinon/issues/2430)) (dependabot\[bot]) > Co-authored-by: dependabot\[bot] <49699333+dependabot\[bot][@​users](https://togithub.com/users).noreply.github.com> - [`a00f14a9`](https://togithub.com/sinonjs/sinon/commit/a00f14a97dbe8c65afa89674e16ad73fc7d2fdc0) Add explicit export for `./*` ([#​2413](https://togithub.com/sinonjs/sinon/issues/2413)) (なつき) - [`b82ca7ad`](https://togithub.com/sinonjs/sinon/commit/b82ca7ad9b1add59007771f65a18ee34415de8ca) Bump cached-path-relative from 1.0.2 to 1.1.0 ([#​2428](https://togithub.com/sinonjs/sinon/issues/2428)) (dependabot\[bot]) - [`a9ea1427`](https://togithub.com/sinonjs/sinon/commit/a9ea142716c094ef3c432ecc4089f8207b8dd8b6) Add documentation for assert.calledOnceWithMatch ([#​2424](https://togithub.com/sinonjs/sinon/issues/2424)) (Mathias Schreck) - [`1d5ab86b`](https://togithub.com/sinonjs/sinon/commit/1d5ab86ba60e50dd69593ffed2bffd4b8faa0d38) Be more general in stripping off stack frames to fix Firefox tests ([#​2425](https://togithub.com/sinonjs/sinon/issues/2425)) (Joel Bradshaw) - [`56b06129`](https://togithub.com/sinonjs/sinon/commit/56b06129e223eae690265c37b1113067e2b31bdc) Check call count type ([#​2410](https://togithub.com/sinonjs/sinon/issues/2410)) (Joel Bradshaw) - [`7863e2df`](https://togithub.com/sinonjs/sinon/commit/7863e2dfdbda79e0a32e42af09e6539fc2f2b80f) Fix [#​2414](https://togithub.com/sinonjs/sinon/issues/2414): make Sinon available on homepage (Carl-Erik Kopseng) - [`fabaabdd`](https://togithub.com/sinonjs/sinon/commit/fabaabdda82f39a7f5b75b55bd56cf77b1cd4a8f) Bump nokogiri from 1.11.4 to 1.13.1 ([#​2423](https://togithub.com/sinonjs/sinon/issues/2423)) (dependabot\[bot]) - [`dbc0fbd2`](https://togithub.com/sinonjs/sinon/commit/dbc0fbd263c8419fa47f9c3b20cf47890a242d21) Bump shelljs from 0.8.4 to 0.8.5 ([#​2422](https://togithub.com/sinonjs/sinon/issues/2422)) (dependabot\[bot]) - [`fb8b3d72`](https://togithub.com/sinonjs/sinon/commit/fb8b3d72a85dc8fb0547f859baf3f03a22a039f7) Run Prettier (Carl-Erik Kopseng) - [`12a45939`](https://togithub.com/sinonjs/sinon/commit/12a45939e9b047b6d3663fe55f2eb383ec63c4e1) Fix 2377: Throw error when trying to stub non-configurable or non-writable properties ([#​2417](https://togithub.com/sinonjs/sinon/issues/2417)) (Stuart Dotson) > Fixes issue [#​2377](https://togithub.com/sinonjs/sinon/issues/2377) by throwing an error when trying to stub non-configurable or non-writable properties *Released by [Carl-Erik Kopseng](https://togithub.com/fatso83) on 2022-01-28.*
--- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb4ff9c4..9104a715 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "nock": "^13.0.0", "null-loader": "^4.0.0", "puppeteer": "^13.0.0", - "sinon": "^12.0.0", + "sinon": "^13.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "^3.8.3", From 3035c9d4ab3e6fd6ca779d99cd3b6fbdaba8b853 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 3 Feb 2022 22:14:48 +0000 Subject: [PATCH 325/662] docs(nodejs): version support policy edits (#1346) (#1357) --- .github/.OwlBot.lock.yaml | 15 ++++++++++++++- README.md | 24 ++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 2c37ca7a..84059c19 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,16 @@ +# Copyright 2022 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. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:89c5b2f3decec8ad64febbebea671076c119d1ab43700da380846a315600de8a + digest: sha256:a9d166a74752226923d159cb723df53429e226c9c076dad3ca52ffd073ff3bb4 diff --git a/README.md b/README.md index 968d6a58..ddb44978 100644 --- a/README.md +++ b/README.md @@ -867,21 +867,21 @@ also contains samples. Our client libraries follow the [Node.js release schedule](https://nodejs.org/en/about/releases/). Libraries are compatible with all current _active_ and _maintenance_ versions of Node.js. +If you are using an end-of-life version of Node.js, we recommend that you update +as soon as possible to an actively supported LTS version. -Client libraries targeting some end-of-life versions of Node.js are available, and -can be installed via npm [dist-tags](https://docs.npmjs.com/cli/dist-tag). -The dist-tags follow the naming convention `legacy-(version)`. - -_Legacy Node.js versions are supported as a best effort:_ +Google's client libraries support legacy versions of Node.js runtimes on a +best-efforts basis with the following warnings: -* Legacy versions will not be tested in continuous integration. -* Some security patches may not be able to be backported. -* Dependencies will not be kept up-to-date, and features will not be backported. +* Legacy versions are not tested in continuous integration. +* Some security patches and features cannot be backported. +* Dependencies cannot be kept up-to-date. -#### Legacy tags available - -* `legacy-8`: install client libraries from this dist-tag for versions - compatible with Node.js 8. +Client libraries targeting some end-of-life versions of Node.js are available, and +can be installed through npm [dist-tags](https://docs.npmjs.com/cli/dist-tag). +The dist-tags follow the naming convention `legacy-(version)`. +For example, `npm install google-auth-library@legacy-8` installs client libraries +for versions compatible with Node.js 8. ## Versioning From 74be65ddd4319ed5acd6ab8aaef5a7bb1e8aa69b Mon Sep 17 00:00:00 2001 From: Michael Angelo <55844504+michaelangrivera@users.noreply.github.com> Date: Tue, 8 Feb 2022 11:43:01 -0500 Subject: [PATCH 326/662] fix spelling (#1350) Co-authored-by: Benjamin E. Coe --- src/auth/authclient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 49a0c01f..78882f11 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -133,7 +133,7 @@ export abstract class AuthClient * that overrides getRequestMetadataAsync(), which is a shared helper for * setting request information in both gRPC and HTTP API calls. * - * @param headers objedcdt to append additional headers to. + * @param headers object to append additional headers to. */ protected addSharedMetadataHeaders(headers: Headers): Headers { // quota_project_id, stored in application_default_credentials.json, is set in From 88f42eca1a02ab5768e02538f2dc639d196de9fb Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 9 Feb 2022 11:40:37 -0800 Subject: [PATCH 327/662] feat: Export `AuthClient` (#1361) Makes it more convenient to use types that extend `AuthClient` --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 4d6519e6..4e1f0ead 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ // limitations under the License. import {GoogleAuth} from './auth/googleauth'; +export {AuthClient} from './auth/authclient'; export {Compute, ComputeOptions} from './auth/computeclient'; export { CredentialBody, From 8c2c35515606ff0a05a3c90fe9f888cc0cc73c9d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:48:09 -0800 Subject: [PATCH 328/662] chore(main): release 7.12.0 (#1362) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73150108..263f0186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.12.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.11.0...v7.12.0) (2022-02-09) + + +### Features + +* Export `AuthClient` ([#1361](https://github.com/googleapis/google-auth-library-nodejs/issues/1361)) ([88f42ec](https://github.com/googleapis/google-auth-library-nodejs/commit/88f42eca1a02ab5768e02538f2dc639d196de9fb)) + ## [7.11.0](https://www.github.com/googleapis/google-auth-library-nodejs/compare/v7.10.4...v7.11.0) (2021-12-15) diff --git a/package.json b/package.json index 9104a715..e396aa36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.11.0", + "version": "7.12.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 60f4a0da..7eac2c69 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.11.0", + "google-auth-library": "^7.12.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 8839b5b12531ae4966b38795ed818ad138eb326a Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 17 Feb 2022 08:55:02 -0800 Subject: [PATCH 329/662] feat: Support instantiating `GoogleAuth` with an `AuthClient` (#1364) --- .readme-partials.yaml | 31 +++++++---------------- README.md | 27 +++++--------------- samples/downscopedclient.js | 24 ++++-------------- samples/test/downscoping-with-cab.test.js | 6 +++-- src/auth/googleauth.ts | 28 +++++++++++++++----- test/test.googleauth.ts | 29 +++++++++++++++++++++ 6 files changed, 76 insertions(+), 69 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index f1bfe053..f055f770 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -439,7 +439,7 @@ body: |- - `$POOL_ID`: The workload identity pool ID. - `$OIDC_PROVIDER_ID`: The OIDC provider ID. - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. - - `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from. + - `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from. This will generate the configuration file in the specified output file. @@ -618,7 +618,7 @@ body: |- An Impersonated Credentials Client is instantiated with a `sourceClient`. This client should use credentials that have the "Service Account Token Creator" role (`roles/iam.serviceAccountTokenCreator`), and should authenticate with the `https://www.googleapis.com/auth/cloud-platform`, or `https://www.googleapis.com/auth/iam` scopes. - + `sourceClient` is used by the Impersonated Credentials Client to impersonate a target service account with a specified set of scopes. @@ -656,7 +656,7 @@ body: |- } // Use impersonated credentials with google-cloud client library - // Note: this works only with certain cloud client libraries utilizing gRPC + // Note: this works only with certain cloud client libraries utilizing gRPC // e.g., SecretManager, KMS, AIPlatform // will not currently work with libraries using REST, e.g., Storage, Compute const smClient = new SecretManagerServiceClient({ @@ -681,14 +681,14 @@ body: |- [Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials) is used to restrict the Identity and Access Management (IAM) permissions that a short-lived credential can use. - The `DownscopedClient` class can be used to produce a downscoped access token from a + The `DownscopedClient` class can be used to produce a downscoped access token from a `CredentialAccessBoundary` and a source credential. The Credential Access Boundary specifies which resources the newly created credential can access, as well as an upper bound on the permissions that are available on each resource. Using downscoped credentials ensures tokens in flight always have the least privileges, e.g. Principle of Least Privilege. > Notice: Only Cloud Storage supports Credential Access Boundaries for now. ### Sample Usage There are two entities needed to generate and use credentials generated from - Downscoped Client with Credential Access Boundaries. + Downscoped Client with Credential Access Boundaries. - Token broker: This is the entity with elevated permissions. This entity has the permissions needed to generate downscoped tokens. The common pattern of usage is to have a token broker with elevated access generate these downscoped credentials from higher access source credentials and pass the downscoped short-lived access tokens to a token consumer via some secure authenticated channel for limited access to Google Cloud Storage resources. @@ -731,10 +731,10 @@ body: |- expiry_date = refreshedAccessToken.expirationTime; ``` - A token broker can be set up on a server in a private network. Various workloads + A token broker can be set up on a server in a private network. Various workloads (token consumers) in the same network will send authenticated requests to that broker for downscoped tokens to access or modify specific google cloud storage buckets. - The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer. + The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer. - Token consumer: This is the consumer of the downscoped tokens. This entity does not have the direct ability to generate access tokens and instead relies on the token broker to provide it with downscoped tokens to run operations on GCS buckets. It is assumed that the downscoped token consumer may have its own mechanism to authenticate itself with the token broker. @@ -763,20 +763,7 @@ body: |- // Use the consumer client to define storageOptions and create a GCS object. const storageOptions = { projectId: 'my_project_id', - authClient: { - sign: () => Promise.reject('unsupported'), - getCredentials: () => Promise.reject(), - request: (opts, callback) => { - return oauth2Client.request(opts, callback); - }, - authorizeRequest: async (opts) => { - opts = opts || {}; - const url = opts.url || opts.uri; - const headers = await oauth2Client.getRequestHeaders(url); - opts.headers = Object.assign(opts.headers || {}, headers); - return opts; - }, - }, + authClient: oauth2Client, }; const storage = new Storage(storageOptions); @@ -788,4 +775,4 @@ body: |- console.log(downloadFile.toString('utf8')); main().catch(console.error); - ``` \ No newline at end of file + ``` diff --git a/README.md b/README.md index ddb44978..99680bfa 100644 --- a/README.md +++ b/README.md @@ -483,7 +483,7 @@ Where the following variables need to be substituted: - `$POOL_ID`: The workload identity pool ID. - `$OIDC_PROVIDER_ID`: The OIDC provider ID. - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. -- `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from. +- `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from. This will generate the configuration file in the specified output file. @@ -700,7 +700,7 @@ async function main() { } // Use impersonated credentials with google-cloud client library - // Note: this works only with certain cloud client libraries utilizing gRPC + // Note: this works only with certain cloud client libraries utilizing gRPC // e.g., SecretManager, KMS, AIPlatform // will not currently work with libraries using REST, e.g., Storage, Compute const smClient = new SecretManagerServiceClient({ @@ -725,14 +725,14 @@ main(); [Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials) is used to restrict the Identity and Access Management (IAM) permissions that a short-lived credential can use. -The `DownscopedClient` class can be used to produce a downscoped access token from a +The `DownscopedClient` class can be used to produce a downscoped access token from a `CredentialAccessBoundary` and a source credential. The Credential Access Boundary specifies which resources the newly created credential can access, as well as an upper bound on the permissions that are available on each resource. Using downscoped credentials ensures tokens in flight always have the least privileges, e.g. Principle of Least Privilege. > Notice: Only Cloud Storage supports Credential Access Boundaries for now. ### Sample Usage There are two entities needed to generate and use credentials generated from -Downscoped Client with Credential Access Boundaries. +Downscoped Client with Credential Access Boundaries. - Token broker: This is the entity with elevated permissions. This entity has the permissions needed to generate downscoped tokens. The common pattern of usage is to have a token broker with elevated access generate these downscoped credentials from higher access source credentials and pass the downscoped short-lived access tokens to a token consumer via some secure authenticated channel for limited access to Google Cloud Storage resources. @@ -775,10 +775,10 @@ access_token = refreshedAccessToken.token; expiry_date = refreshedAccessToken.expirationTime; ``` -A token broker can be set up on a server in a private network. Various workloads +A token broker can be set up on a server in a private network. Various workloads (token consumers) in the same network will send authenticated requests to that broker for downscoped tokens to access or modify specific google cloud storage buckets. -The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer. +The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer. - Token consumer: This is the consumer of the downscoped tokens. This entity does not have the direct ability to generate access tokens and instead relies on the token broker to provide it with downscoped tokens to run operations on GCS buckets. It is assumed that the downscoped token consumer may have its own mechanism to authenticate itself with the token broker. @@ -807,20 +807,7 @@ oauth2Client.refreshHandler = async () => { // Use the consumer client to define storageOptions and create a GCS object. const storageOptions = { projectId: 'my_project_id', - authClient: { - sign: () => Promise.reject('unsupported'), - getCredentials: () => Promise.reject(), - request: (opts, callback) => { - return oauth2Client.request(opts, callback); - }, - authorizeRequest: async (opts) => { - opts = opts || {}; - const url = opts.url || opts.uri; - const headers = await oauth2Client.getRequestHeaders(url); - opts.headers = Object.assign(opts.headers || {}, headers); - return opts; - }, - }, + authClient: oauth2Client, }; const storage = new Storage(storageOptions); diff --git a/samples/downscopedclient.js b/samples/downscopedclient.js index edd10ea5..47e5d054 100644 --- a/samples/downscopedclient.js +++ b/samples/downscopedclient.js @@ -54,7 +54,6 @@ async function main() { }, }; - const oauth2Client = new OAuth2Client(); const googleAuth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform', }); @@ -63,6 +62,9 @@ async function main() { const client = await googleAuth.getClient(); // Use the client to generate a DownscopedClient. const cabClient = new DownscopedClient(client, cab); + + // OAuth 2.0 Client + const oauth2Client = new OAuth2Client(); // Define a refreshHandler that will be used to refresh the downscoped token // when it expires. oauth2Client.refreshHandler = async () => { @@ -75,24 +77,7 @@ async function main() { const storageOptions = { projectId, - authClient: { - getCredentials: async () => { - Promise.reject(); - }, - request: opts => { - return oauth2Client.request(opts); - }, - sign: () => { - Promise.reject('unsupported'); - }, - authorizeRequest: async opts => { - opts = opts || {}; - const url = opts.url || opts.uri; - const headers = await oauth2Client.getRequestHeaders(url); - opts.headers = Object.assign(opts.headers || {}, headers); - return opts; - }, - }, + authClient: new GoogleAuth({auth: oauth2Client}), }; const storage = new Storage(storageOptions); @@ -100,6 +85,7 @@ async function main() { .bucket(bucketName) .file(objectName) .download(); + console.log('Successfully retrieved file. Contents:'); console.log(downloadFile.toString('utf8')); } diff --git a/samples/test/downscoping-with-cab.test.js b/samples/test/downscoping-with-cab.test.js index b8bc6dee..55f0912c 100644 --- a/samples/test/downscoping-with-cab.test.js +++ b/samples/test/downscoping-with-cab.test.js @@ -30,9 +30,9 @@ const {promisify} = require('util'); const exec = promisify(cp.exec); // Copy values from the output of samples/scripts/downscoping-with-cab-setup.js. // GCS bucket name. -const bucketName = 'cab-int-bucket-z2zsauf4sj'; +const bucketName = 'cab-int-bucket-brd3qlsuok'; // GCS object name. -const objectName = 'cab-first-z2zsauf4sj.txt'; +const objectName = 'cab-first-"brd3qlsuok.txt'; /** * Runs the provided command using asynchronous child_process.exec. @@ -59,7 +59,9 @@ describe('samples for downscoping with cab', () => { OBJECT_NAME: objectName, }, }); + // Confirm expected script output. + assert.match(output, /Successfully retrieved file/); assert.match(output, /first/); }); }); diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index b6e2ff0e..6ddaee3a 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -72,6 +72,10 @@ export interface ADCResponse { } export interface GoogleAuthOptions { + /** + * An `AuthClient` to use + */ + auth?: AuthClient; /** * Path to a .json, .pem, or .p12 key file */ @@ -135,7 +139,8 @@ export class GoogleAuth { // To save the contents of the JSON credential file jsonContent: JWTInput | ExternalAccountClientOptions | null = null; - cachedCredential: JSONClient | Impersonated | Compute | null = null; + cachedCredential: JSONClient | Impersonated | Compute | AuthClient | null = + null; /** * Scopes populated by the client library by default. We differentiate between @@ -153,7 +158,9 @@ export class GoogleAuth { constructor(opts?: GoogleAuthOptions) { opts = opts || {}; + this._cachedProjectId = opts.projectId || null; + this.cachedCredential = opts.auth || null; this.keyFilename = opts.keyFilename || opts.keyFile; this.scopes = opts.scopes; this.jsonContent = opts.credentials || null; @@ -270,7 +277,7 @@ export class GoogleAuth { // If we've already got a cached credential, just return it. if (this.cachedCredential) { return { - credential: this.cachedCredential as JSONClient, + credential: this.cachedCredential, projectId: await this.getProjectIdAsync(), }; } @@ -313,7 +320,10 @@ export class GoogleAuth { try { isGCE = await this._checkIsGCE(); } catch (e) { - e.message = `Unexpected error determining execution environment: ${e.message}`; + if (e instanceof Error) { + e.message = `Unexpected error determining execution environment: ${e.message}`; + } + throw e; } @@ -364,7 +374,10 @@ export class GoogleAuth { options ); } catch (e) { - e.message = `Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable: ${e.message}`; + if (e instanceof Error) { + e.message = `Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable: ${e.message}`; + } + throw e; } } @@ -438,7 +451,10 @@ export class GoogleAuth { throw new Error(); } } catch (err) { - err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`; + if (err instanceof Error) { + err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`; + } + throw err; } @@ -511,7 +527,7 @@ export class GoogleAuth { // cache both raw data used to instantiate client and client itself. this.jsonContent = json; this.cachedCredential = client; - return this.cachedCredential; + return client; } /** diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 5266d9f6..c7f40f73 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -285,6 +285,35 @@ describe('googleauth', () => { return sandbox.stub(process, 'env').value(envVars); } + it('should accept and use an `AuthClient`', async () => { + const customRequestHeaders = { + 'my-unique': 'header', + }; + + // Using a custom `AuthClient` to ensure any `AuthClient` would work + class MyAuthClient extends AuthClient { + async getAccessToken() { + return {token: '', res: undefined}; + } + + async getRequestHeaders() { + return {...customRequestHeaders}; + } + + request = OAuth2Client.prototype.request.bind(this); + } + + const authClient = new MyAuthClient(); + + const auth = new GoogleAuth({ + auth: authClient, + }); + + assert.equal(auth.cachedCredential, authClient); + assert.equal(await auth.getClient(), authClient); + assert.deepEqual(await auth.getRequestHeaders(''), customRequestHeaders); + }); + it('fromJSON should support the instantiated named export', () => { const result = auth.fromJSON(createJwtJSON()); assert(result); From 6ff40e559968e99e82864463af6b94f1227bd792 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 17 Feb 2022 09:13:23 -0800 Subject: [PATCH 330/662] chore(main): release 7.13.0 (#1367) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 263f0186..c4b7e545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.13.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.12.0...v7.13.0) (2022-02-17) + + +### Features + +* Support instantiating `GoogleAuth` with an `AuthClient` ([#1364](https://github.com/googleapis/google-auth-library-nodejs/issues/1364)) ([8839b5b](https://github.com/googleapis/google-auth-library-nodejs/commit/8839b5b12531ae4966b38795ed818ad138eb326a)) + ## [7.12.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.11.0...v7.12.0) (2022-02-09) diff --git a/package.json b/package.json index e396aa36..77c9b192 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.12.0", + "version": "7.13.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 7eac2c69..3d1f3855 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.12.0", + "google-auth-library": "^7.13.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 8373000b2240cb694e9492f849e5cc7e13c89b1a Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 17 Feb 2022 15:09:47 -0800 Subject: [PATCH 331/662] fix: Rename `auth` to `authClient` & Use Generics for `AuthClient` (#1371) --- samples/downscopedclient.js | 6 +++--- src/auth/googleauth.ts | 13 ++++++------- test/test.googleauth.ts | 4 +--- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/samples/downscopedclient.js b/samples/downscopedclient.js index 47e5d054..c33d51d5 100644 --- a/samples/downscopedclient.js +++ b/samples/downscopedclient.js @@ -64,10 +64,10 @@ async function main() { const cabClient = new DownscopedClient(client, cab); // OAuth 2.0 Client - const oauth2Client = new OAuth2Client(); + const authClient = new OAuth2Client(); // Define a refreshHandler that will be used to refresh the downscoped token // when it expires. - oauth2Client.refreshHandler = async () => { + authClient.refreshHandler = async () => { const refreshedAccessToken = await cabClient.getAccessToken(); return { access_token: refreshedAccessToken.token, @@ -77,7 +77,7 @@ async function main() { const storageOptions = { projectId, - authClient: new GoogleAuth({auth: oauth2Client}), + authClient: new GoogleAuth({authClient}), }; const storage = new Storage(storageOptions); diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 6ddaee3a..19f05f3d 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -71,11 +71,11 @@ export interface ADCResponse { projectId: string | null; } -export interface GoogleAuthOptions { +export interface GoogleAuthOptions { /** * An `AuthClient` to use */ - auth?: AuthClient; + authClient?: T; /** * Path to a .json, .pem, or .p12 key file */ @@ -115,7 +115,7 @@ export interface GoogleAuthOptions { export const CLOUD_SDK_CLIENT_ID = '764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com'; -export class GoogleAuth { +export class GoogleAuth { transporter?: Transporter; /** @@ -139,8 +139,7 @@ export class GoogleAuth { // To save the contents of the JSON credential file jsonContent: JWTInput | ExternalAccountClientOptions | null = null; - cachedCredential: JSONClient | Impersonated | Compute | AuthClient | null = - null; + cachedCredential: JSONClient | Impersonated | Compute | T | null = null; /** * Scopes populated by the client library by default. We differentiate between @@ -156,11 +155,11 @@ export class GoogleAuth { */ static DefaultTransporter = DefaultTransporter; - constructor(opts?: GoogleAuthOptions) { + constructor(opts?: GoogleAuthOptions) { opts = opts || {}; this._cachedProjectId = opts.projectId || null; - this.cachedCredential = opts.auth || null; + this.cachedCredential = opts.authClient || null; this.keyFilename = opts.keyFilename || opts.keyFile; this.scopes = opts.scopes; this.jsonContent = opts.credentials || null; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index c7f40f73..8ec8f264 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -305,9 +305,7 @@ describe('googleauth', () => { const authClient = new MyAuthClient(); - const auth = new GoogleAuth({ - auth: authClient, - }); + const auth = new GoogleAuth({authClient}); assert.equal(auth.cachedCredential, authClient); assert.equal(await auth.getClient(), authClient); From 9ea3e98582e8a69dedef89952ae08d64c49f48ef Mon Sep 17 00:00:00 2001 From: sai-sunder-s <4540365+sai-sunder-s@users.noreply.github.com> Date: Tue, 22 Feb 2022 20:07:26 +0000 Subject: [PATCH 332/662] feat: Add AWS Session Token to Metadata Requests (#1363) See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot Co-authored-by: Benjamin E. Coe --- linkinator.config.json | 3 +- src/auth/awsclient.ts | 67 ++++++++++++++++++++++++++++++++++-------- test/test.awsclient.ts | 42 ++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/linkinator.config.json b/linkinator.config.json index 29a223b6..48cfef38 100644 --- a/linkinator.config.json +++ b/linkinator.config.json @@ -3,7 +3,8 @@ "skip": [ "https://codecov.io/gh/googleapis/", "www.googleapis.com", - "img.shields.io" + "img.shields.io", + "http://169.254.169.254/latest/api/token%22" ], "silent": true, "concurrency": 10 diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index 9995e2f7..7a61fd17 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -19,7 +19,7 @@ import { BaseExternalAccountClient, BaseExternalAccountClientOptions, } from './baseexternalclient'; -import {RefreshOptions} from './oauth2client'; +import {RefreshOptions, Headers} from './oauth2client'; /** * AWS credentials JSON interface. This is used for AWS workloads. @@ -36,6 +36,12 @@ export interface AwsClientOptions extends BaseExternalAccountClientOptions { // environment variables. url?: string; regional_cred_verification_url: string; + // The imdsv2 session token url is used to fetch session token from AWS + // which is later sent through headers for metadata requests. If the + // field is missing, then session token won't be fetched and sent with + // the metadata requests. + // The session token is required for IMDSv2 but optional for IMDSv1 + imdsv2_session_token_url?: string; }; } @@ -62,6 +68,7 @@ export class AwsClient extends BaseExternalAccountClient { private readonly regionUrl?: string; private readonly securityCredentialsUrl?: string; private readonly regionalCredVerificationUrl: string; + private readonly imdsV2SessionTokenUrl?: string; private awsRequestSigner: AwsRequestSigner | null; private region: string; @@ -86,6 +93,8 @@ export class AwsClient extends BaseExternalAccountClient { this.securityCredentialsUrl = options.credential_source.url; this.regionalCredVerificationUrl = options.credential_source.regional_cred_verification_url; + this.imdsV2SessionTokenUrl = + options.credential_source.imdsv2_session_token_url; const match = this.environmentId?.match(/^(aws)(\d+)$/); if (!match || !this.regionalCredVerificationUrl) { throw new Error('No valid AWS "credential_source" provided'); @@ -106,22 +115,32 @@ export class AwsClient extends BaseExternalAccountClient { * this uses a serialized AWS signed request to the STS GetCallerIdentity * endpoint. * The logic is summarized as: - * 1. Retrieve AWS region from availability-zone. - * 2a. Check AWS credentials in environment variables. If not found, get + * 1. If imdsv2_session_token_url is provided in the credential source, then + * fetch the aws session token and include it in the headers of the + * metadata requests. This is a requirement for IDMSv2 but optional + * for IDMSv1. + * 2. Retrieve AWS region from availability-zone. + * 3a. Check AWS credentials in environment variables. If not found, get * from security-credentials endpoint. - * 2b. Get AWS credentials from security-credentials endpoint. In order + * 3b. Get AWS credentials from security-credentials endpoint. In order * to retrieve this, the AWS role needs to be determined by calling * security-credentials endpoint without any argument. Then the * credentials can be retrieved via: security-credentials/role_name - * 3. Generate the signed request to AWS STS GetCallerIdentity action. - * 4. Inject x-goog-cloud-target-resource into header and serialize the + * 4. Generate the signed request to AWS STS GetCallerIdentity action. + * 5. Inject x-goog-cloud-target-resource into header and serialize the * signed request. This will be the subject-token to pass to GCP STS. * @return A promise that resolves with the external subject token. */ async retrieveSubjectToken(): Promise { // Initialize AWS request signer if not already initialized. if (!this.awsRequestSigner) { - this.region = await this.getAwsRegion(); + const metadataHeaders: Headers = {}; + if (this.imdsV2SessionTokenUrl) { + metadataHeaders['x-aws-ec2-metadata-token'] = + await this.getImdsV2SessionToken(); + } + + this.region = await this.getAwsRegion(metadataHeaders); this.awsRequestSigner = new AwsRequestSigner(async () => { // Check environment variables for permanent credentials first. // https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html @@ -137,12 +156,15 @@ export class AwsClient extends BaseExternalAccountClient { }; } // Since the role on a VM can change, we don't need to cache it. - const roleName = await this.getAwsRoleName(); + const roleName = await this.getAwsRoleName(metadataHeaders); // Temporary credentials typically last for several hours. // Expiration is returned in response. // Consider future optimization of this logic to cache AWS tokens // until their natural expiration. - const awsCreds = await this.getAwsSecurityCredentials(roleName); + const awsCreds = await this.getAwsSecurityCredentials( + roleName, + metadataHeaders + ); return { accessKeyId: awsCreds.AccessKeyId, secretAccessKey: awsCreds.SecretAccessKey, @@ -198,9 +220,24 @@ export class AwsClient extends BaseExternalAccountClient { } /** + * @return A promise that resolves with the IMDSv2 Session Token. + */ + private async getImdsV2SessionToken(): Promise { + const opts: GaxiosOptions = { + url: this.imdsV2SessionTokenUrl, + method: 'PUT', + responseType: 'text', + headers: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }; + const response = await this.transporter.request(opts); + return response.data; + } + + /** + * @param headers The headers to be used in the metadata request. * @return A promise that resolves with the current AWS region. */ - private async getAwsRegion(): Promise { + private async getAwsRegion(headers: Headers): Promise { // Priority order for region determination: // AWS_REGION > AWS_DEFAULT_REGION > metadata server. if (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION']) { @@ -216,6 +253,7 @@ export class AwsClient extends BaseExternalAccountClient { url: this.regionUrl, method: 'GET', responseType: 'text', + headers: headers, }; const response = await this.transporter.request(opts); // Remove last character. For example, if us-east-2b is returned, @@ -224,10 +262,11 @@ export class AwsClient extends BaseExternalAccountClient { } /** + * @param headers The headers to be used in the metadata request. * @return A promise that resolves with the assigned role to the current * AWS VM. This is needed for calling the security-credentials endpoint. */ - private async getAwsRoleName(): Promise { + private async getAwsRoleName(headers: Headers): Promise { if (!this.securityCredentialsUrl) { throw new Error( 'Unable to determine AWS role name due to missing ' + @@ -238,6 +277,7 @@ export class AwsClient extends BaseExternalAccountClient { url: this.securityCredentialsUrl, method: 'GET', responseType: 'text', + headers: headers, }; const response = await this.transporter.request(opts); return response.data; @@ -247,15 +287,18 @@ export class AwsClient extends BaseExternalAccountClient { * Retrieves the temporary AWS credentials by calling the security-credentials * endpoint as specified in the `credential_source` object. * @param roleName The role attached to the current VM. + * @param headers The headers to be used in the metadata request. * @return A promise that resolves with the temporary AWS credentials * needed for creating the GetCallerIdentity signed request. */ private async getAwsSecurityCredentials( - roleName: string + roleName: string, + headers: Headers ): Promise { const response = await this.transporter.request({ url: `${this.securityCredentialsUrl}/${roleName}`, responseType: 'json', + headers: headers, }); return response.data; } diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 9ce18920..762e9a30 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -44,6 +44,7 @@ describe('AwsClient', () => { const secretAccessKey = awsSecurityCredentials.SecretAccessKey; const token = awsSecurityCredentials.Token; const awsRole = 'gcp-aws-role'; + const awsSessionToken = 'sessiontoken'; const audience = getAudience(); const metadataBaseUrl = 'http://169.254.169.254'; const awsCredentialSource = { @@ -265,6 +266,47 @@ describe('AwsClient', () => { scope.done(); }); + it('should resolve on success with imdsv2 session token', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }) + .put('/latest/api/token') + .reply(200, awsSessionToken) + ); + + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, + }) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials) + ); + + const credentialSourceWithSessionTokenUrl = Object.assign( + {imdsv2_session_token_url: `${metadataBaseUrl}/latest/api/token`}, + awsCredentialSource + ); + const awsOptionsWithSessionTokenUrl = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: credentialSourceWithSessionTokenUrl, + }; + + const client = new AwsClient(awsOptionsWithSessionTokenUrl); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scopes.forEach(scope => scope.done()); + }); + it('should resolve on success with permanent creds', async () => { const permanentAwsSecurityCredentials = Object.assign( {}, From 502b47efb3420a600a9494cdb6b2270d42538b4b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 15:22:05 -0500 Subject: [PATCH 333/662] chore(main): release 7.14.0 (#1372) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b7e545..11cf57ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [7.14.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.13.0...v7.14.0) (2022-02-22) + + +### Features + +* Add AWS Session Token to Metadata Requests ([#1363](https://github.com/googleapis/google-auth-library-nodejs/issues/1363)) ([9ea3e98](https://github.com/googleapis/google-auth-library-nodejs/commit/9ea3e98582e8a69dedef89952ae08d64c49f48ef)) + + +### Bug Fixes + +* Rename `auth` to `authClient` & Use Generics for `AuthClient` ([#1371](https://github.com/googleapis/google-auth-library-nodejs/issues/1371)) ([8373000](https://github.com/googleapis/google-auth-library-nodejs/commit/8373000b2240cb694e9492f849e5cc7e13c89b1a)) + ## [7.13.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.12.0...v7.13.0) (2022-02-17) diff --git a/package.json b/package.json index 77c9b192..5535fde1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.13.0", + "version": "7.14.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 3d1f3855..5af490f8 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.13.0", + "google-auth-library": "^7.14.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 9e45be92d32da44f564cbdacf3d367ca5a7c1779 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 26 Feb 2022 12:10:36 +0100 Subject: [PATCH 334/662] chore(deps): update actions/setup-node action to v3 (#1376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-node](https://togithub.com/actions/setup-node) | action | major | `v1` -> `v3` | --- ### Release Notes
actions/setup-node ### [`v3`](https://togithub.com/actions/setup-node/compare/v2...v3) [Compare Source](https://togithub.com/actions/setup-node/compare/v2...v3) ### [`v2`](https://togithub.com/actions/setup-node/compare/v1...v2) [Compare Source](https://togithub.com/actions/setup-node/compare/v1...v2)
--- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). From 22cd652bf430e44ac3250e48606691b07592fb62 Mon Sep 17 00:00:00 2001 From: sai-sunder-s <4540365+sai-sunder-s@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:01:44 +0000 Subject: [PATCH 335/662] docs: Update readme for AWS IMDSv2 (#1375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Update readme for AWS IMDSv2 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot Co-authored-by: Jeffrey Rennie --- .readme-partials.yaml | 4 ++++ README.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index f055f770..1d48f317 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -362,6 +362,10 @@ body: |- This will generate the configuration file in the specified output file. + If you want to use the AWS IMDSv2 flow, you can add the field below to the credential_source in your AWS ADC configuration file: + "imdsv2_session_token_url": "http://169.254.169.254/latest/api/token" + The gcloud create-cred-config command will be updated to support this soon. + You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from AWS. ### Access resources from Microsoft Azure diff --git a/README.md b/README.md index 99680bfa..91d4139b 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,10 @@ Where the following variables need to be substituted: This will generate the configuration file in the specified output file. +If you want to use the AWS IMDSv2 flow, you can add the field below to the credential_source in your AWS ADC configuration file: +"imdsv2_session_token_url": "http://169.254.169.254/latest/api/token" +The gcloud create-cred-config command will be updated to support this soon. + You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from AWS. ### Access resources from Microsoft Azure From 8d476445b73a6fc7a562848f7d0c26013446b0fd Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 2 Mar 2022 12:36:30 +0100 Subject: [PATCH 336/662] chore(deps): update actions/checkout action to v3 (#1378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/checkout](https://togithub.com/actions/checkout) | action | major | `v2` -> `v3` | --- ### Release Notes
actions/checkout ### [`v3`](https://togithub.com/actions/checkout/compare/v2...v3) [Compare Source](https://togithub.com/actions/checkout/compare/v2...v3)
--- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). From db27f1bc31efaa9df28da2e0a1229ee3ebea0751 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Wed, 9 Mar 2022 12:28:17 -0800 Subject: [PATCH 337/662] fix(serverless): clean up ID token example (#1380) --- samples/idtokens-serverless.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index 5d2dc48b..701b91ad 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -28,9 +28,7 @@ function main( const {URL} = require('url'); targetAudience = new URL(url).origin; } - // [START google_auth_idtoken_serverless] // [START cloudrun_service_to_service_auth] - // [START run_service_to_service_auth] // [START functions_bearer_token] /** * TODO(developer): Uncomment these variables before running the sample. @@ -38,9 +36,13 @@ function main( // Example: https://my-cloud-run-service.run.app/books/delete/12345 // const url = 'https://TARGET_HOSTNAME/TARGET_URL'; + // [END functions_bearer_token] // Example (Cloud Run): https://my-cloud-run-service.run.app/ + // [START functions_bearer_token] + // [END cloudrun_service_to_service_auth] // Example (Cloud Functions): https://project-region-projectid.cloudfunctions.net/myFunction - // const targetAudience = 'https://TARGET_HOSTNAME/'; + // [START cloudrun_service_to_service_auth] + // const targetAudience = 'https://TARGET_AUDIENCE/'; const {GoogleAuth} = require('google-auth-library'); const auth = new GoogleAuth(); @@ -56,9 +58,7 @@ function main( process.exitCode = 1; }); // [END functions_bearer_token] - // [END run_service_to_service_auth] // [END cloudrun_service_to_service_auth] - // [END google_auth_idtoken_serverless] } const args = process.argv.slice(2); From 54cfaaf5a44070fa12fb2a3ba5d7d1549d780c42 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 23 Mar 2022 18:02:32 +0000 Subject: [PATCH 338/662] chore(main): release 7.14.1 (#1381) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11cf57ed..5ee55119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [7.14.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.14.0...v7.14.1) (2022-03-09) + + +### Bug Fixes + +* **serverless:** clean up ID token example ([#1380](https://github.com/googleapis/google-auth-library-nodejs/issues/1380)) ([db27f1b](https://github.com/googleapis/google-auth-library-nodejs/commit/db27f1bc31efaa9df28da2e0a1229ee3ebea0751)) + ## [7.14.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.13.0...v7.14.0) (2022-02-22) diff --git a/package.json b/package.json index 5535fde1..70450d2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.14.0", + "version": "7.14.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 5af490f8..f0d1e038 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.14.0", + "google-auth-library": "^7.14.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 3fce2bd4fcc54c0dc7cf278ff2bb848c394641a4 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 13:31:25 -0700 Subject: [PATCH 339/662] chore: Enable Size-Label bot in all googleapis NodeJs repositories (#1382) (#1387) * chore: Enable Size-Label bot in all googleapis NodeJs repositories Auto-label T-shirt size indicator should be assigned on every new pull request in all googleapis NodeJs repositories * Remove product Remove product since it is by default true Source-Link: https://github.com/googleapis/synthtool/commit/f1562fa1c219d7176f79e3eea611b268c361e93d Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:bb4d47d0e770abad62699a4664ce6b9ff1629d50c276a6c75860a6a1853dd19b Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 3 ++- .github/auto-label.yaml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .github/auto-label.yaml diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 84059c19..c6ddf44f 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,4 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:a9d166a74752226923d159cb723df53429e226c9c076dad3ca52ffd073ff3bb4 + digest: sha256:bb4d47d0e770abad62699a4664ce6b9ff1629d50c276a6c75860a6a1853dd19b +# created: 2022-04-01T19:19:56.587347289Z diff --git a/.github/auto-label.yaml b/.github/auto-label.yaml new file mode 100644 index 00000000..09c8d735 --- /dev/null +++ b/.github/auto-label.yaml @@ -0,0 +1,2 @@ +requestsize: + enabled: true From 2c8c44cee0e56b2946392d6b2e31ac8ad406c5ae Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 15:58:11 +0000 Subject: [PATCH 340/662] chore(deps): update actions/setup-node action to v3 (#1393) (#1388) Co-authored-by: Jeffrey Rennie Source-Link: https://github.com/googleapis/synthtool/commit/6593fb2234deff0444032cb2a91100bde4985caf Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:1d25dfefd805b689a2a2356d35a25b13f2f67bcce55400246432c43a42e96214 --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index c6ddf44f..ba38c131 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:bb4d47d0e770abad62699a4664ce6b9ff1629d50c276a6c75860a6a1853dd19b -# created: 2022-04-01T19:19:56.587347289Z + digest: sha256:1d25dfefd805b689a2a2356d35a25b13f2f67bcce55400246432c43a42e96214 +# created: 2022-04-05T22:42:50.409517925Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a9517884..1edb2065 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: node: [10, 12, 14, 16] steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - run: node --version @@ -30,7 +30,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: 14 - run: npm install @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: 14 - run: npm install @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: 14 - run: npm install From b48254490768799e465a8fa4aae13296ddceea53 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 20:20:11 +0000 Subject: [PATCH 341/662] chore(deps): update actions/checkout action to v3 (#1392) (#1389) Co-authored-by: Jeffrey Rennie Source-Link: https://github.com/googleapis/synthtool/commit/9368bc795a376954920c374406e92efb0e3d0ac4 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:f74e740638e66be7ced1540626217dbb72980eb73885b2339a70592f38c9ff2c --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index ba38c131..b4c08f9a 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:1d25dfefd805b689a2a2356d35a25b13f2f67bcce55400246432c43a42e96214 -# created: 2022-04-05T22:42:50.409517925Z + digest: sha256:f74e740638e66be7ced1540626217dbb72980eb73885b2339a70592f38c9ff2c +# created: 2022-04-06T18:36:33.987617127Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1edb2065..35414632 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [10, 12, 14, 16] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 14 @@ -49,7 +49,7 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 14 From 416ea9315780f36c7cd9e29e5d700cc73f799738 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 11 Apr 2022 15:23:26 -0700 Subject: [PATCH 342/662] chore(deps): Update `gtoken`'s minimum version (#1390) Helps customers avoid using an insecure version of `gtoken` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70450d2a..2af0488c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "fast-text-encoding": "^1.0.0", "gaxios": "^4.0.0", "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", + "gtoken": "^5.3.2", "jws": "^4.0.0", "lru-cache": "^6.0.0" }, From 9c02941e52514f8e3b07fedb01439fc313046063 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 12 Apr 2022 13:29:40 -0700 Subject: [PATCH 343/662] refactor!: remove deprecated DeprecatedGetClientOptions (#1393) --- src/auth/googleauth.ts | 10 +--------- src/auth/oauth2client.ts | 1 - test/test.googleauth.ts | 8 -------- test/test.oauth2.ts | 8 -------- 4 files changed, 1 insertion(+), 26 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 19f05f3d..e84402ce 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -59,9 +59,6 @@ export interface CredentialCallback { (err: Error | null, result?: JSONClient): void; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface DeprecatedGetClientOptions {} - export interface ADCCallback { (err: Error | null, credential?: AuthClient, projectId?: string | null): void; } @@ -790,12 +787,7 @@ export class GoogleAuth { * Automatically obtain a client based on the provided configuration. If no * options were passed, use Application Default Credentials. */ - async getClient(options?: DeprecatedGetClientOptions) { - if (options) { - throw new Error( - 'Passing options to getClient is forbidden in v5.0.0. Use new GoogleAuth(opts) instead.' - ); - } + async getClient() { if (!this.cachedCredential) { if (this.jsonContent) { this._cacheClientFromJSON(this.jsonContent, this.clientOptions); diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 0fdf8dcf..34ff13d4 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -709,7 +709,6 @@ export class OAuth2Client extends AuthClient { /** * Retrieves the access token using refresh token * - * @deprecated use getRequestHeaders instead. * @param callback callback */ refreshAccessToken(): Promise; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 8ec8f264..f9b601cb 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1496,14 +1496,6 @@ describe('googleauth', () => { ); }); - it('should throw if options are passed to getClient()', async () => { - const auth = new GoogleAuth(); - await assert.rejects( - auth.getClient({hello: 'world'}), - /Passing options to getClient is forbidden in v5.0.0/ - ); - }); - it('getRequestHeaders populates x-goog-user-project with quota_project if present', async () => { const tokenReq = mockApplicationDefaultCredentials( './test/fixtures/config-with-quota' diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index ea85ff1e..763eb329 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -900,14 +900,6 @@ describe('oauth2', () => { }); }); - it('should not emit warning on refreshAccessToken', async () => { - let warned = false; - sandbox.stub(process, 'emitWarning').callsFake(() => (warned = true)); - client.refreshAccessToken(() => { - assert.strictEqual(warned, false); - }); - }); - it('should return error in callback on refreshAccessToken', done => { client.refreshAccessToken((err, result) => { assert.strictEqual(err!.message, 'No refresh token is set.'); From 3614c16ce8e66db2f397060a26d95687016bf60c Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 14 Apr 2022 10:54:21 -0700 Subject: [PATCH 344/662] build: make ci testing conditional on engines field in package.json, move configs to Node 12 (#1418) (#1395) * build: make ci testing conditional on engines field in package.json, move configs to Node 12 Co-authored-by: Benjamin E. Coe Source-Link: https://github.com/googleapis/synthtool/commit/2800f5a85af0e0399c71a63169a53ade3e0d42f6 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:dc7bfb4c4bf50496abbdd24bd9e4aaa833dc75248c0a9e3a7f807feda5258873 Co-authored-by: Owl Bot Co-authored-by: Benjamin E. Coe --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 2 +- .kokoro/common.cfg | 2 +- .kokoro/release/docs.cfg | 2 +- .kokoro/samples-test.sh | 2 +- .kokoro/system-test.sh | 2 +- .kokoro/test.sh | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index b4c08f9a..11e07e9c 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:f74e740638e66be7ced1540626217dbb72980eb73885b2339a70592f38c9ff2c -# created: 2022-04-06T18:36:33.987617127Z + digest: sha256:dc7bfb4c4bf50496abbdd24bd9e4aaa833dc75248c0a9e3a7f807feda5258873 +# created: 2022-04-14T17:36:54.629564643Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 35414632..25251dbb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 03e6b50b..8e9e508e 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -16,7 +16,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/release/docs.cfg b/.kokoro/release/docs.cfg index 8f8a1316..02c51cf2 100644 --- a/.kokoro/release/docs.cfg +++ b/.kokoro/release/docs.cfg @@ -11,7 +11,7 @@ before_action { # doc publications use a Python image. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" } # Download trampoline resources. diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index f249d3e4..fbc058a4 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -56,7 +56,7 @@ fi # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=10 +COVERAGE_NODE=12 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 0a840452..87fa0653 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -49,7 +49,7 @@ npm run system-test # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=10 +COVERAGE_NODE=12 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/test.sh b/.kokoro/test.sh index af1ce7e3..a5c7ac04 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -39,7 +39,7 @@ npm test # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=10 +COVERAGE_NODE=12 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then From e4a5430eef1f28288c6634986aecfb0cb5e8faae Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 14 Apr 2022 21:56:20 +0000 Subject: [PATCH 345/662] build: add srs yaml file (#1396) * build: add sync-repo-settings and change branch protection Source-Link: https://github.com/googleapis/synthtool/commit/ed8079c3d155b3ec820361f3d4d847715c7c1623 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:80bfa0c67226453b37b501be7748b2fa2a2676cfeec0012e79e3a1a8f1cbe6a3 --- .github/.OwlBot.lock.yaml | 4 ++-- .github/sync-repo-settings.yaml | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 .github/sync-repo-settings.yaml diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 11e07e9c..4ed84dc1 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:dc7bfb4c4bf50496abbdd24bd9e4aaa833dc75248c0a9e3a7f807feda5258873 -# created: 2022-04-14T17:36:54.629564643Z + digest: sha256:80bfa0c67226453b37b501be7748b2fa2a2676cfeec0012e79e3a1a8f1cbe6a3 +# created: 2022-04-14T19:57:08.518420247Z diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 00000000..1b362683 --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,17 @@ +branchProtectionRules: + - pattern: main + isAdminEnforced: true + requiredApprovingReviewCount: 1 + requiresCodeOwnerReviews: true + requiresStrictStatusChecks: false + requiredStatusCheckContexts: + - "ci/kokoro: Samples test" + - "ci/kokoro: System test" + - docs + - lint + - test (10) + - test (12) + - test (14) + - cla/google + - windows + - OwlBot Post Processor From b7bcedb9e4ec24217a861016f3378460af7598c7 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 20 Apr 2022 08:39:42 -0700 Subject: [PATCH 346/662] build!: Set Node v12 to minimum supported version & Upgrade TypeScript (#1392) --- .github/sync-repo-settings.yaml | 2 +- .github/workflows/ci.yaml | 2 +- .kokoro/continuous/node10/common.cfg | 34 ----------------------- .kokoro/continuous/node10/docs.cfg | 4 --- .kokoro/continuous/node10/test.cfg | 9 ------ .kokoro/continuous/node8/browser-test.cfg | 12 -------- .kokoro/continuous/node8/common.cfg | 24 ---------------- .kokoro/continuous/node8/test.cfg | 0 .kokoro/presubmit/node10/common.cfg | 34 ----------------------- .kokoro/presubmit/node10/docs.cfg | 4 --- .kokoro/presubmit/node10/lint.cfg | 4 --- .kokoro/presubmit/node10/test.cfg | 0 package.json | 6 ++-- samples/package.json | 2 +- samples/puppeteer/package.json | 2 +- src/auth/baseexternalclient.ts | 2 +- src/auth/computeclient.ts | 12 ++++++-- src/auth/identitypoolclient.ts | 5 +++- src/auth/impersonated.ts | 13 +++++++-- src/auth/oauth2client.ts | 19 ++++++++++--- src/auth/stscredentials.ts | 4 +-- src/transporters.ts | 2 +- test/test.awsclient.ts | 18 +++++++++--- test/test.externalclient.ts | 3 +- test/test.googleauth.ts | 5 ++-- test/test.jwt.ts | 6 ++-- test/test.jwtaccess.ts | 4 +-- test/test.refresh.ts | 6 ++-- 28 files changed, 77 insertions(+), 161 deletions(-) delete mode 100644 .kokoro/continuous/node10/common.cfg delete mode 100644 .kokoro/continuous/node10/docs.cfg delete mode 100644 .kokoro/continuous/node10/test.cfg delete mode 100644 .kokoro/continuous/node8/browser-test.cfg delete mode 100644 .kokoro/continuous/node8/common.cfg delete mode 100644 .kokoro/continuous/node8/test.cfg delete mode 100644 .kokoro/presubmit/node10/common.cfg delete mode 100644 .kokoro/presubmit/node10/docs.cfg delete mode 100644 .kokoro/presubmit/node10/lint.cfg delete mode 100644 .kokoro/presubmit/node10/test.cfg diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 1b362683..d1e8b5e6 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -9,9 +9,9 @@ branchProtectionRules: - "ci/kokoro: System test" - docs - lint - - test (10) - test (12) - test (14) + - test (16) - cla/google - windows - OwlBot Post Processor diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 25251dbb..f447b84a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14] + node: [12, 14, 16] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/.kokoro/continuous/node10/common.cfg b/.kokoro/continuous/node10/common.cfg deleted file mode 100644 index d144aee1..00000000 --- a/.kokoro/continuous/node10/common.cfg +++ /dev/null @@ -1,34 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Bring in codecov.io token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "dpebot_codecov_token" - } - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/test.sh" -} diff --git a/.kokoro/continuous/node10/docs.cfg b/.kokoro/continuous/node10/docs.cfg deleted file mode 100644 index 213beda5..00000000 --- a/.kokoro/continuous/node10/docs.cfg +++ /dev/null @@ -1,4 +0,0 @@ -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/docs.sh" -} diff --git a/.kokoro/continuous/node10/test.cfg b/.kokoro/continuous/node10/test.cfg deleted file mode 100644 index 609c0cf0..00000000 --- a/.kokoro/continuous/node10/test.cfg +++ /dev/null @@ -1,9 +0,0 @@ -# Bring in codecov.io token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "dpebot_codecov_token" - } - } -} diff --git a/.kokoro/continuous/node8/browser-test.cfg b/.kokoro/continuous/node8/browser-test.cfg deleted file mode 100644 index fb5125fb..00000000 --- a/.kokoro/continuous/node8/browser-test.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Download resources for system tests (service account key, etc.) -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:8-puppeteer" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/browser-test.sh" -} diff --git a/.kokoro/continuous/node8/common.cfg b/.kokoro/continuous/node8/common.cfg deleted file mode 100644 index e3a36c7f..00000000 --- a/.kokoro/continuous/node8/common.cfg +++ /dev/null @@ -1,24 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:8-user" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/test.sh" -} diff --git a/.kokoro/continuous/node8/test.cfg b/.kokoro/continuous/node8/test.cfg deleted file mode 100644 index e69de29b..00000000 diff --git a/.kokoro/presubmit/node10/common.cfg b/.kokoro/presubmit/node10/common.cfg deleted file mode 100644 index d144aee1..00000000 --- a/.kokoro/presubmit/node10/common.cfg +++ /dev/null @@ -1,34 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Bring in codecov.io token into the build as $KOKORO_KEYSTORE_DIR/73713_dpebot_codecov_token -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "dpebot_codecov_token" - } - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/test.sh" -} diff --git a/.kokoro/presubmit/node10/docs.cfg b/.kokoro/presubmit/node10/docs.cfg deleted file mode 100644 index 213beda5..00000000 --- a/.kokoro/presubmit/node10/docs.cfg +++ /dev/null @@ -1,4 +0,0 @@ -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/docs.sh" -} diff --git a/.kokoro/presubmit/node10/lint.cfg b/.kokoro/presubmit/node10/lint.cfg deleted file mode 100644 index 49ffcd82..00000000 --- a/.kokoro/presubmit/node10/lint.cfg +++ /dev/null @@ -1,4 +0,0 @@ -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/lint.sh" -} diff --git a/.kokoro/presubmit/node10/test.cfg b/.kokoro/presubmit/node10/test.cfg deleted file mode 100644 index e69de29b..00000000 diff --git a/package.json b/package.json index 2af0488c..f43a5aa0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { - "node": ">=10" + "node": ">=12" }, "main": "./build/src/index.js", "types": "./build/src/index.d.ts", @@ -44,7 +44,7 @@ "chai": "^4.2.0", "codecov": "^3.0.2", "execa": "^5.0.0", - "gts": "^2.0.0", + "gts": "^3.1.0", "is-docker": "^2.0.0", "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", @@ -65,7 +65,7 @@ "sinon": "^13.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", - "typescript": "^3.8.3", + "typescript": "^4.6.3", "webpack": "^5.21.2", "webpack-cli": "^4.0.0" }, diff --git a/samples/package.json b/samples/package.json index f0d1e038..e26fc245 100644 --- a/samples/package.json +++ b/samples/package.json @@ -9,7 +9,7 @@ "test": "mocha --timeout 60000" }, "engines": { - "node": ">=10" + "node": ">=12" }, "license": "Apache-2.0", "dependencies": { diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index ce54cfb0..9c38a8b1 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -4,7 +4,7 @@ "description": "An example of using puppeteer to orchestrate a Google sign in flow.", "main": "oauth2-test.js", "engines": { - "node": ">=8" + "node": ">=12" }, "scripts": { "start": "node oauth2-test.js" diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 691e3964..bf13d368 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -247,7 +247,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { * the type of external credential used. * @return A promise that resolves with the external subject token. */ - abstract async retrieveSubjectToken(): Promise; + abstract retrieveSubjectToken(): Promise; /** * @return A promise that resolves with the current GCP access token diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 1bfb4d51..db91df01 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -73,8 +73,11 @@ export class Compute extends OAuth2Client { } data = await gcpMetadata.instance(instanceOptions); } catch (e) { - e.message = `Could not refresh access token: ${e.message}`; - this.wrapError(e); + if (e instanceof GaxiosError) { + e.message = `Could not refresh access token: ${e.message}`; + this.wrapError(e); + } + throw e; } const tokens = data as Credentials; @@ -101,7 +104,10 @@ export class Compute extends OAuth2Client { }; idToken = await gcpMetadata.instance(instanceOptions); } catch (e) { - e.message = `Could not fetch ID token: ${e.message}`; + if (e instanceof Error) { + e.message = `Could not fetch ID token: ${e.message}`; + } + throw e; } diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index eed39b1c..102025ed 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -155,7 +155,10 @@ export class IdentityPoolClient extends BaseExternalAccountClient { throw new Error(); } } catch (err) { - err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`; + if (err instanceof Error) { + err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`; + } + throw err; } diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index a6646095..fb1085f6 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -16,6 +16,7 @@ import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; import {AuthClient} from './authclient'; +import {GaxiosError} from 'gaxios'; export interface ImpersonatedOptions extends RefreshOptions { /** @@ -132,8 +133,16 @@ export class Impersonated extends OAuth2Client { res, }; } catch (error) { - const status = error?.response?.data?.error?.status; - const message = error?.response?.data?.error?.message; + if (!(error instanceof Error)) throw error; + + let status = 0; + let message = ''; + + if (error instanceof GaxiosError) { + status = error?.response?.data?.error?.status; + message = error?.response?.data?.error?.message; + } + if (status && message) { error.message = `${status}: unable to impersonate: ${message}`; throw error; diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 34ff13d4..de70e452 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -1155,7 +1155,10 @@ export class OAuth2Client extends AuthClient { try { res = await this.transporter.request({url}); } catch (e) { - e.message = `Failed to retrieve verification certificates: ${e.message}`; + if (e instanceof Error) { + e.message = `Failed to retrieve verification certificates: ${e.message}`; + } + throw e; } @@ -1220,7 +1223,10 @@ export class OAuth2Client extends AuthClient { try { res = await this.transporter.request({url}); } catch (e) { - e.message = `Failed to retrieve verification certificates: ${e.message}`; + if (e instanceof Error) { + e.message = `Failed to retrieve verification certificates: ${e.message}`; + } + throw e; } @@ -1271,7 +1277,10 @@ export class OAuth2Client extends AuthClient { try { envelope = JSON.parse(crypto.decodeBase64StringUtf8(segments[0])); } catch (err) { - err.message = `Can't parse token envelope: ${segments[0]}': ${err.message}`; + if (err instanceof Error) { + err.message = `Can't parse token envelope: ${segments[0]}': ${err.message}`; + } + throw err; } @@ -1282,7 +1291,9 @@ export class OAuth2Client extends AuthClient { try { payload = JSON.parse(crypto.decodeBase64StringUtf8(segments[1])); } catch (err) { - err.message = `Can't parse token payload '${segments[0]}`; + if (err instanceof Error) { + err.message = `Can't parse token payload '${segments[0]}`; + } throw err; } diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 497b1e76..52178b82 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {GaxiosOptions, GaxiosResponse} from 'gaxios'; +import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; import * as querystring from 'querystring'; import {DefaultTransporter} from '../transporters'; @@ -216,7 +216,7 @@ export class StsCredentials extends OAuthClientAuthHandler { return stsSuccessfulResponse; } catch (error) { // Translate error to OAuthError. - if (error.response) { + if (error instanceof GaxiosError && error.response) { throw getErrorFromOAuthErrorResponse( error.response.data as OAuthErrorResponse, // Preserve other fields from the original error. diff --git a/src/transporters.ts b/src/transporters.ts index 3b3fded6..952f22af 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -104,7 +104,7 @@ export class DefaultTransporter { validate(opts); } catch (e) { if (callback) { - return callback(e); + return callback(e as Error); } else { throw e; } diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 762e9a30..496d6599 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -408,7 +408,9 @@ describe('AwsClient', () => { {}, awsCredentialSource ); - delete missingUrlCredentialSource.url; + delete ( + missingUrlCredentialSource as Partial + ).url; const invalidOptions = { type: 'external_account', audience, @@ -435,7 +437,11 @@ describe('AwsClient', () => { {}, awsCredentialSource ); - delete missingRegionUrlCredentialSource.region_url; + delete ( + missingRegionUrlCredentialSource as Partial< + typeof awsCredentialSource + > + ).region_url; const invalidOptions = { type: 'external_account', audience, @@ -707,8 +713,12 @@ describe('AwsClient', () => { awsCredentialSource ); // Remove all optional fields. - delete requiredOnlyCredentialSource.region_url; - delete requiredOnlyCredentialSource.url; + delete ( + requiredOnlyCredentialSource as Partial + ).region_url; + delete ( + requiredOnlyCredentialSource as Partial + ).url; const requiredOnlyOptions = { type: 'external_account', audience, diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index f341d343..645ac30c 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -191,7 +191,8 @@ describe('ExternalAccountClient', () => { it('should throw when given invalid ExternalAccountClient', () => { const invalidOptions = Object.assign({}, fileSourcedOptions); - delete invalidOptions.credential_source; + delete (invalidOptions as Partial) + .credential_source; assert.throws(() => { return ExternalAccountClient.fromJSON(invalidOptions); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index f9b601cb..ef5bc3cf 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -406,7 +406,7 @@ describe('googleauth', () => { it('fromJSON should error on missing client_email', () => { const json = createJwtJSON(); - delete json.client_email; + delete (json as Partial).client_email; assert.throws(() => { auth.fromJSON(json); }); @@ -414,7 +414,7 @@ describe('googleauth', () => { it('fromJSON should error on missing private_key', () => { const json = createJwtJSON(); - delete json.private_key; + delete (json as Partial).private_key; assert.throws(() => { auth.fromJSON(json); }); @@ -1600,6 +1600,7 @@ describe('googleauth', () => { try { await auth.getIdTokenClient('a-target-audience'); } catch (e) { + assert(e instanceof Error); assert( e.message.startsWith('Cannot fetch ID token in this environment') ); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 274fe889..6f94f281 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -633,7 +633,7 @@ describe('jwt', () => { it('should error on missing client_id', () => { const json = createRefreshJSON(); - delete json.client_id; + delete (json as Partial).client_id; const jwt = new JWT(); assert.throws(() => { jwt.fromJSON(json); @@ -642,7 +642,7 @@ describe('jwt', () => { it('should error on missing client_secret', () => { const json = createRefreshJSON(); - delete json.client_secret; + delete (json as Partial).client_secret; const jwt = new JWT(); assert.throws(() => { jwt.fromJSON(json); @@ -651,7 +651,7 @@ describe('jwt', () => { it('should error on missing refresh_token', () => { const json = createRefreshJSON(); - delete json.refresh_token; + delete (json as Partial).refresh_token; const jwt = new JWT(); assert.throws(() => { jwt.fromJSON(json); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index 438e84c8..1434f256 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -128,7 +128,7 @@ describe('jwtaccess', () => { it('fromJson should error on missing client_email', () => { const j = Object.assign({}, json); - delete j.client_email; + delete (j as Partial).client_email; assert.throws(() => { client.fromJSON(j); }); @@ -136,7 +136,7 @@ describe('jwtaccess', () => { it('fromJson should error on missing private_key', () => { const j = Object.assign({}, json); - delete j.private_key; + delete (j as Partial).private_key; assert.throws(() => { client.fromJSON(j); }); diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 2e06d7de..4f7b888f 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -55,7 +55,7 @@ describe('refresh', () => { it('fromJSON should error on missing client_id', () => { const json = createJSON(); - delete json.client_id; + delete (json as Partial).client_id; const refresh = new UserRefreshClient(); assert.throws(() => { refresh.fromJSON(json); @@ -64,7 +64,7 @@ describe('refresh', () => { it('fromJSON should error on missing client_secret', () => { const json = createJSON(); - delete json.client_secret; + delete (json as Partial).client_secret; const refresh = new UserRefreshClient(); assert.throws(() => { refresh.fromJSON(json); @@ -73,7 +73,7 @@ describe('refresh', () => { it('fromJSON should error on missing refresh_token', () => { const json = createJSON(); - delete json.refresh_token; + delete (json as Partial).refresh_token; const refresh = new UserRefreshClient(); assert.throws(() => { refresh.fromJSON(json); From 2cc0415ad72df07e7a91a0b1b7595d50b62f036f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 20 Apr 2022 12:15:21 -0700 Subject: [PATCH 347/662] chore(main): release 8.0.0 (#1394) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 17 +++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee55119..32fe5d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.0.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.14.1...v8.0.0) (2022-04-20) + + +### ⚠ BREAKING CHANGES + +* Set Node v12 to minimum supported version & Upgrade TypeScript (#1392) +* remove deprecated DeprecatedGetClientOptions (#1393) + +### Code Refactoring + +* remove deprecated DeprecatedGetClientOptions ([#1393](https://github.com/googleapis/google-auth-library-nodejs/issues/1393)) ([9c02941](https://github.com/googleapis/google-auth-library-nodejs/commit/9c02941e52514f8e3b07fedb01439fc313046063)) + + +### Build System + +* Set Node v12 to minimum supported version & Upgrade TypeScript ([#1392](https://github.com/googleapis/google-auth-library-nodejs/issues/1392)) ([b7bcedb](https://github.com/googleapis/google-auth-library-nodejs/commit/b7bcedb9e4ec24217a861016f3378460af7598c7)) + ### [7.14.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.14.0...v7.14.1) (2022-03-09) diff --git a/package.json b/package.json index f43a5aa0..b439434d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "7.14.1", + "version": "8.0.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index e26fc245..b3014c73 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", - "google-auth-library": "^7.14.1", + "google-auth-library": "^8.0.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 9a8be636eaea979979426558dca40def9c0e569e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 21 Apr 2022 17:41:52 +0200 Subject: [PATCH 348/662] fix(deps): update dependency google-auth-library to v8 (#1399) --- samples/puppeteer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 9c38a8b1..793d54e5 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -11,7 +11,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^7.0.0", + "google-auth-library": "^8.0.0", "puppeteer": "^13.0.0" } } From cbd7d4f471046afa05ff4539c9a93bba998b318c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Apr 2022 20:52:53 +0200 Subject: [PATCH 349/662] fix(deps): update dependency gaxios to v5 (#1398) --- package.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b439434d..6a09ab5b 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", "gtoken": "^5.3.2", "jws": "^4.0.0", "lru-cache": "^6.0.0" @@ -51,12 +51,11 @@ "karma-coverage": "^2.0.0", "karma-firefox-launcher": "^2.0.0", "karma-mocha": "^2.0.0", - "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^5.0.0", "keypair": "^1.0.4", - "linkinator": "^2.0.0", - "mocha": "^8.0.0", + "linkinator": "^3.0.3", + "mocha": "^9.2.2", "mv": "^2.1.1", "ncp": "^2.0.0", "nock": "^13.0.0", From 5df4753160c0117b3d2376e0dafd795a349ae7e6 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 22 Apr 2022 14:59:01 -0400 Subject: [PATCH 350/662] chore(main): release 8.0.1 (#1400) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32fe5d7f..50fe045a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [8.0.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.0.0...v8.0.1) (2022-04-22) + + +### Bug Fixes + +* **deps:** update dependency gaxios to v5 ([#1398](https://github.com/googleapis/google-auth-library-nodejs/issues/1398)) ([cbd7d4f](https://github.com/googleapis/google-auth-library-nodejs/commit/cbd7d4f471046afa05ff4539c9a93bba998b318c)) +* **deps:** update dependency google-auth-library to v8 ([#1399](https://github.com/googleapis/google-auth-library-nodejs/issues/1399)) ([9a8be63](https://github.com/googleapis/google-auth-library-nodejs/commit/9a8be636eaea979979426558dca40def9c0e569e)) + ## [8.0.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v7.14.1...v8.0.0) (2022-04-20) diff --git a/package.json b/package.json index 6a09ab5b..5b6c33b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.0.0", + "version": "8.0.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index b3014c73..b47e132d 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", - "google-auth-library": "^8.0.0", + "google-auth-library": "^8.0.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From b0ddb7512fb9ed1e51b6874b7376d7e1f26be644 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Wed, 27 Apr 2022 22:26:57 +0000 Subject: [PATCH 351/662] fix: Fixing Implementation of GoogleAuth.sign() for external account credentials (#1397) * fix: Fixing Implementation of GoogleAuth.sign() for external account credentials Currently, creating signed storage URLs does not work for external account credentials because the storage library expects client_email to be returned from GoogleAuth.getCredentials(). Changing the logic so the same client email that is used to the sign the blob (extracted from the Service Account Impersonation URL) is returned from the getCredentials() call. Fixes #1239 * addressing code review comments * addressing code review comments --- src/auth/googleauth.ts | 32 ++++++++++++-------------------- test/test.googleauth.ts | 16 +++++++++++++++- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index e84402ce..f4aad6f1 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -730,8 +730,10 @@ export class GoogleAuth { /** * The callback function handles a credential object that contains the * client_email and private_key (if exists). - * getCredentials checks for these values from the user JSON at first. - * If it doesn't exist, and the environment is on GCE, it gets the + * getCredentials first checks if the client is using an external account and + * uses the service account email in place of client_email. + * If that doesn't exist, it checks for these values from the user JSON. + * If the user JSON doesn't exist, and the environment is on GCE, it gets the * client_email from the cloud metadata server. * @param callback Callback that handles the credential object that contains * a client_email and optional private key, or the error. @@ -752,7 +754,14 @@ export class GoogleAuth { } private async getCredentialsAsync(): Promise { - await this.getClient(); + const client = await this.getClient(); + + if (client instanceof BaseExternalAccountClient) { + const serviceAccountEmail = client.getServiceAccountEmail(); + if (serviceAccountEmail) { + return {client_email: serviceAccountEmail}; + } + } if (this.jsonContent) { const credential: CredentialBody = { @@ -884,23 +893,6 @@ export class GoogleAuth { 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 BaseExternalAccountClient && - client.getServiceAccountEmail() - ) { - return this.signBlob( - crypto, - client.getServiceAccountEmail() as string, - data - ); - } - const projectId = await this.getProjectId(); if (!projectId) { throw new Error('Cannot sign data without a project ID.'); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index ef5bc3cf..c2bc6bee 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -2239,7 +2239,7 @@ describe('googleauth', () => { it('should use IAMCredentials endpoint when impersonation is used', async () => { const scopes = mockGetAccessTokenAndProjectId( - false, + true, ['https://www.googleapis.com/auth/cloud-platform'], true ); @@ -2340,6 +2340,20 @@ describe('googleauth', () => { assert.deepStrictEqual(res.data, data); scopes.forEach(s => s.done()); }); + + describe('getCredentials()', () => { + it('getCredentials() should return the service account email for external accounts', async () => { + // Set up a mock to return path to a valid credentials file. + const email = saEmail; + const configWithImpersonation = createExternalAccountJSON(); + configWithImpersonation.service_account_impersonation_url = + getServiceAccountImpersonationUrl(); + const auth = new GoogleAuth({credentials: configWithImpersonation}); + const body = await auth.getCredentials(); + assert.notStrictEqual(null, body); + assert.strictEqual(email, body.client_email); + }); + }); }); }); From 003df0f1594faec5b37521413f111e77b230ec82 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 29 Apr 2022 11:07:39 -0700 Subject: [PATCH 352/662] chore(main): release 8.0.2 (#1401) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50fe045a..74592371 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +### [8.0.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.0.1...v8.0.2) (2022-04-27) + + +### Bug Fixes + +* Fixing Implementation of GoogleAuth.sign() for external account credentials ([#1397](https://github.com/googleapis/google-auth-library-nodejs/issues/1397)) ([b0ddb75](https://github.com/googleapis/google-auth-library-nodejs/commit/b0ddb7512fb9ed1e51b6874b7376d7e1f26be644)) + ### [8.0.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.0.0...v8.0.1) (2022-04-22) diff --git a/package.json b/package.json index 5b6c33b6..43708925 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.0.1", + "version": "8.0.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index b47e132d..bf2febd0 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^5.15.4", "@googleapis/iam": "^2.0.0", - "google-auth-library": "^8.0.1", + "google-auth-library": "^8.0.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 19534a9b424457bd79b991c57c35f615db253f8b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 3 May 2022 02:32:27 +0200 Subject: [PATCH 353/662] chore(deps): update dependency @types/mocha to v9 (#1403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [@types/mocha](https://togithub.com/DefinitelyTyped/DefinitelyTyped) | [`^8.0.0` -> `^9.0.0`](https://renovatebot.com/diffs/npm/@types%2fmocha/8.2.3/9.1.1) | [![age](https://badges.renovateapi.com/packages/npm/@types%2fmocha/9.1.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/@types%2fmocha/9.1.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/@types%2fmocha/9.1.1/compatibility-slim/8.2.3)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/@types%2fmocha/9.1.1/confidence-slim/8.2.3)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43708925..e620c527 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", "@types/lru-cache": "^5.0.0", - "@types/mocha": "^8.0.0", + "@types/mocha": "^9.0.0", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", "@types/node": "^16.0.0", From b12da109173f062036db3d6e9944e5e25e657b94 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 6 May 2022 18:44:24 +0000 Subject: [PATCH 354/662] build: update auto approve to v2, remove release autoapproving (#1432) (#1408) * build: update auto-approve file to v2 Source-Link: https://github.com/googleapis/synthtool/commit/19eb6fc07dc178a682da6d186dc874017a166438 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:b9e4584a1fe3c749e3c37c92497b13dce653b2e694f0261f0610eb0e15941357 --- .github/.OwlBot.lock.yaml | 4 ++-- .github/auto-approve.yml | 15 +++------------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 4ed84dc1..9acbabb1 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:80bfa0c67226453b37b501be7748b2fa2a2676cfeec0012e79e3a1a8f1cbe6a3 -# created: 2022-04-14T19:57:08.518420247Z + digest: sha256:b9e4584a1fe3c749e3c37c92497b13dce653b2e694f0261f0610eb0e15941357 +# created: 2022-05-05T21:08:42.530332893Z diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml index 49cf9422..4cd91cc1 100644 --- a/.github/auto-approve.yml +++ b/.github/auto-approve.yml @@ -1,12 +1,3 @@ -rules: -- author: "release-please[bot]" - title: "^chore: release" - changedFiles: - - "package\\.json$" - - "CHANGELOG\\.md$" - maxFiles: 3 -- author: "renovate-bot" - title: "^(fix|chore)\\(deps\\):" - changedFiles: - - "package\\.json$" - maxFiles: 2 +processes: + - "NodeDependency" + - "OwlBotTemplateChanges" From d7893c1dc70b3aa45c12bb9b6c0e5346a293b130 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 9 May 2022 17:34:33 +0200 Subject: [PATCH 355/662] chore(deps): update dependency sinon to v14 (#1410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [sinon](https://sinonjs.org/) ([source](https://togithub.com/sinonjs/sinon)) | [`^13.0.0` -> `^14.0.0`](https://renovatebot.com/diffs/npm/sinon/13.0.2/14.0.0) | [![age](https://badges.renovateapi.com/packages/npm/sinon/14.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/sinon/14.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/sinon/14.0.0/compatibility-slim/13.0.2)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/sinon/14.0.0/confidence-slim/13.0.2)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
sinonjs/sinon ### [`v14.0.0`](https://togithub.com/sinonjs/sinon/blob/HEAD/CHANGES.md#​1400) [Compare Source](https://togithub.com/sinonjs/sinon/compare/v13.0.2...v14.0.0) - [`c2bbd826`](https://togithub.com/sinonjs/sinon/commit/c2bbd82641444eb5b32822489ae40f185afbbf00) Drop node 12 (Morgan Roderick) > And embrace Node 18 > > See https://nodejs.org/en/about/releases/ *Released by Morgan Roderick on 2022-05-07.*
--- ### Configuration 📅 **Schedule**: "after 9am and before 3pm" (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e620c527..9665779f 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "nock": "^13.0.0", "null-loader": "^4.0.0", "puppeteer": "^13.0.0", - "sinon": "^13.0.0", + "sinon": "^14.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "^4.6.3", From 1667711e092fa6bd2fa49fc39d45b618749d8606 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 2 Jun 2022 20:34:21 +0200 Subject: [PATCH 356/662] chore(deps): update dependency @google-cloud/storage to v6 (#1416) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index bf2febd0..7071e371 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@google-cloud/storage": "^5.15.4", + "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^2.0.0", "google-auth-library": "^8.0.2", "node-fetch": "^2.3.0", From ea34106d22e25a21ed2ff94c667975cb0eb22b79 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 2 Jun 2022 20:48:50 +0200 Subject: [PATCH 357/662] chore(deps): update dependency gtoken to v6 (#1413) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9665779f..0f4e0016 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "fast-text-encoding": "^1.0.0", "gaxios": "^5.0.0", "gcp-metadata": "^5.0.0", - "gtoken": "^5.3.2", + "gtoken": "^6.0.0", "jws": "^4.0.0", "lru-cache": "^6.0.0" }, From c61086847ec82c197e7e8836bced36ea51a5b4d9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 2 Jun 2022 20:59:37 +0200 Subject: [PATCH 358/662] chore(deps): update dependency puppeteer to v14 (#1412) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0f4e0016..68d2aadb 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^13.0.0", + "puppeteer": "^14.0.0", "sinon": "^14.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 793d54e5..7bd95492 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^8.0.0", - "puppeteer": "^13.0.0" + "puppeteer": "^14.0.0" } } From 65101fa2b6d100e34b06783914c76a9df185a047 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 22:04:22 +0000 Subject: [PATCH 359/662] build(node): add new jsteam + enforce branches up-to-date (#1451) (#1420) Source-Link: https://github.com/googleapis/synthtool/commit/cd785291d51d97003d1263056cd2b9de1849a0ab Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:ddb19a6df6c1fa081bc99fb29658f306dd64668bc26f75d1353b28296f3a78e6 --- .github/.OwlBot.lock.yaml | 4 ++-- .github/sync-repo-settings.yaml | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 9acbabb1..f3ca5561 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:b9e4584a1fe3c749e3c37c92497b13dce653b2e694f0261f0610eb0e15941357 -# created: 2022-05-05T21:08:42.530332893Z + digest: sha256:ddb19a6df6c1fa081bc99fb29658f306dd64668bc26f75d1353b28296f3a78e6 +# created: 2022-06-07T21:18:30.024751809Z diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index d1e8b5e6..4a30a08e 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -3,7 +3,7 @@ branchProtectionRules: isAdminEnforced: true requiredApprovingReviewCount: 1 requiresCodeOwnerReviews: true - requiresStrictStatusChecks: false + requiresStrictStatusChecks: true requiredStatusCheckContexts: - "ci/kokoro: Samples test" - "ci/kokoro: System test" @@ -15,3 +15,10 @@ branchProtectionRules: - cla/google - windows - OwlBot Post Processor +permissionRules: + - team: yoshi-admins + permission: admin + - team: jsteam-admins + permission: admin + - team: jsteam + permission: push From 0dc88572958520ddd4834aa982d41b98851895d9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 17 Jun 2022 23:59:49 +0200 Subject: [PATCH 360/662] fix(deps): update dependency @googleapis/iam to v3 (#1421) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 7071e371..5ba14f69 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^6.0.0", - "@googleapis/iam": "^2.0.0", + "@googleapis/iam": "^3.0.0", "google-auth-library": "^8.0.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 672818be8f24afd605eadbf923d48faabe7ff737 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 12:33:21 -0700 Subject: [PATCH 361/662] chore(main): release 8.0.3 (#1423) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74592371..a9cf5102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.0.3](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.0.2...v8.0.3) (2022-06-17) + + +### Bug Fixes + +* **deps:** update dependency @googleapis/iam to v3 ([#1421](https://github.com/googleapis/google-auth-library-nodejs/issues/1421)) ([0dc8857](https://github.com/googleapis/google-auth-library-nodejs/commit/0dc88572958520ddd4834aa982d41b98851895d9)) + ### [8.0.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.0.1...v8.0.2) (2022-04-27) diff --git a/package.json b/package.json index 68d2aadb..b53b2391 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.0.2", + "version": "8.0.3", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 5ba14f69..a1946a54 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.0.2", + "google-auth-library": "^8.0.3", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 835be89687c2dff19f34e8b55645d3d611339e14 Mon Sep 17 00:00:00 2001 From: cstanger <36767130+cstanger@users.noreply.github.com> Date: Thu, 30 Jun 2022 17:25:50 +0200 Subject: [PATCH 362/662] feat: handle impersonated ADC (#1425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: handle impersonated ADC * fix: linting of fromImpersonatedADC * test: add test for impersonated ACD * doc: add impersonated ADC capabilities to readme * fix: resolve code review for impersonated ADC * fix: resolve targetScopes typing for impersonated ADC * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot --- src/auth/credentials.ts | 7 ++ src/auth/googleauth.ts | 73 ++++++++++++++++++- src/auth/impersonated.ts | 2 + ...nated_application_default_credentials.json | 11 +++ test/test.googleauth.ts | 39 ++++++++++ test/test.impersonated.ts | 49 ++++++++++++- 6 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/impersonated_application_default_credentials.json diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index a50499b2..ce149b63 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -78,6 +78,13 @@ export interface JWTInput { quota_project_id?: string; } +export interface ImpersonatedJWTInput { + type?: string; + source_credentials?: JWTInput; + service_account_impersonation_url?: string; + delegates?: string[]; +} + export interface CredentialBody { client_email?: string; private_key?: string; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index f4aad6f1..cc7be332 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -24,13 +24,17 @@ import {Crypto, createCrypto} from '../crypto/crypto'; import {DefaultTransporter, Transporter} from '../transporters'; import {Compute, ComputeOptions} from './computeclient'; -import {CredentialBody, JWTInput} from './credentials'; +import {CredentialBody, ImpersonatedJWTInput, JWTInput} from './credentials'; import {IdTokenClient} from './idtokenclient'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; import {Headers, OAuth2ClientOptions, RefreshOptions} from './oauth2client'; import {UserRefreshClient, UserRefreshClientOptions} from './refreshclient'; -import {Impersonated, ImpersonatedOptions} from './impersonated'; +import { + Impersonated, + ImpersonatedOptions, + IMPERSONATED_ACCOUNT_TYPE, +} from './impersonated'; import { ExternalAccountClient, ExternalAccountClientOptions, @@ -459,13 +463,72 @@ export class GoogleAuth { return this.fromStream(readStream, options); } + /** + * Create a credentials instance using a given impersonated input options. + * @param json The impersonated input object. + * @returns JWT or UserRefresh Client with data + */ + fromImpersonatedJSON(json: ImpersonatedJWTInput): Impersonated { + if (!json) { + throw new Error( + 'Must pass in a JSON object containing an impersonated refresh token' + ); + } + if (json.type !== IMPERSONATED_ACCOUNT_TYPE) { + throw new Error( + `The incoming JSON object does not have the "${IMPERSONATED_ACCOUNT_TYPE}" type` + ); + } + if (!json.source_credentials) { + throw new Error( + 'The incoming JSON object does not contain a source_credentials field' + ); + } + if (!json.service_account_impersonation_url) { + throw new Error( + 'The incoming JSON object does not contain a service_account_impersonation_url field' + ); + } + + // Create source client for impersonation + const sourceClient = new UserRefreshClient( + json.source_credentials.client_id, + json.source_credentials.client_secret, + json.source_credentials.refresh_token + ); + + // Extreact service account from service_account_impersonation_url + const targetPrincipal = /(?[^/]+):generateAccessToken$/.exec( + json.service_account_impersonation_url + )?.groups?.target; + + if (!targetPrincipal) { + throw new RangeError( + `Cannot extract target principal from ${json.service_account_impersonation_url}` + ); + } + + const targetScopes = this.getAnyScopes() ?? []; + + const client = new Impersonated({ + delegates: json.delegates ?? [], + sourceClient: sourceClient, + targetPrincipal: targetPrincipal, + targetScopes: Array.isArray(targetScopes) ? targetScopes : [targetScopes], + }); + return client; + } + /** * 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: JWTInput, options?: RefreshOptions): JSONClient { + fromJSON( + json: JWTInput | ImpersonatedJWTInput, + options?: RefreshOptions + ): JSONClient { let client: JSONClient; if (!json) { throw new Error( @@ -476,6 +539,8 @@ export class GoogleAuth { if (json.type === 'authorized_user') { client = new UserRefreshClient(options); client.fromJSON(json); + } else if (json.type === IMPERSONATED_ACCOUNT_TYPE) { + client = this.fromImpersonatedJSON(json as ImpersonatedJWTInput); } else if (json.type === EXTERNAL_ACCOUNT_TYPE) { client = ExternalAccountClient.fromJSON( json as ExternalAccountClientOptions, @@ -508,6 +573,8 @@ export class GoogleAuth { if (json.type === 'authorized_user') { client = new UserRefreshClient(options); client.fromJSON(json); + } else if (json.type === IMPERSONATED_ACCOUNT_TYPE) { + client = this.fromImpersonatedJSON(json as ImpersonatedJWTInput); } else if (json.type === EXTERNAL_ACCOUNT_TYPE) { client = ExternalAccountClient.fromJSON( json as ExternalAccountClientOptions, diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index fb1085f6..061faec0 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -45,6 +45,8 @@ export interface ImpersonatedOptions extends RefreshOptions { endpoint?: string; } +export const IMPERSONATED_ACCOUNT_TYPE = 'impersonated_service_account'; + export interface TokenResponse { accessToken: string; expireTime: string; diff --git a/test/fixtures/impersonated_application_default_credentials.json b/test/fixtures/impersonated_application_default_credentials.json new file mode 100644 index 00000000..691768a0 --- /dev/null +++ b/test/fixtures/impersonated_application_default_credentials.json @@ -0,0 +1,11 @@ +{ + "delegates": [], + "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken", + "source_credentials": { + "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com", + "client_secret": "privatekey", + "refresh_token": "refreshtoken", + "type": "authorized_user" + }, + "type": "impersonated_service_account" +} \ No newline at end of file diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index c2bc6bee..2bf13828 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -39,6 +39,7 @@ import { OAuth2Client, ExternalAccountClientOptions, RefreshOptions, + Impersonated, } from '../src'; import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; @@ -2183,6 +2184,44 @@ describe('googleauth', () => { scopes.forEach(s => s.done()); }); + it('should initialize from impersonated ADC', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/impersonated_application_default_credentials.json' + ); + + // Set up a mock to explicity return the Project ID, as needed for impersonated ADC + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + + const auth = new GoogleAuth(); + const client = await auth.getClient(); + + assert(client instanceof Impersonated); + + // Check if targetPrincipal gets extracted and used correctly + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + const scopes = [ + nock('https://oauth2.googleapis.com').post('/token').reply(200, { + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken' + ) + .reply(200, { + accessToken: 'qwerty345', + expireTime: tomorrow.toISOString(), + }), + ]; + + await client.refreshAccessToken(); + scopes.forEach(s => s.done()); + assert.strictEqual(client.credentials.access_token, 'qwerty345'); + }); + it('should allow use defaultScopes when no scopes are available', async () => { const keyFilename = './test/fixtures/external-account-cred.json'; const auth = new GoogleAuth({keyFilename}); diff --git a/test/test.impersonated.ts b/test/test.impersonated.ts index 9f2a9d8d..51de9592 100644 --- a/test/test.impersonated.ts +++ b/test/test.impersonated.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import * as nock from 'nock'; import {describe, it, afterEach} from 'mocha'; -import {Impersonated, JWT} from '../src'; +import {Impersonated, JWT, UserRefreshClient} from '../src'; import {CredentialRequest} from '../src/auth/credentials'; const PEM_PATH = './test/fixtures/private.pem'; @@ -204,6 +204,53 @@ describe('impersonated', () => { scopes.forEach(s => s.done()); }); + it('handles authenticating with UserRefreshClient as sourceClient', async () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const scopes = [ + nock(url).get('/').reply(200), + nock('https://oauth2.googleapis.com').post('/token').reply(200, { + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', + (body: ImpersonatedCredentialRequest) => { + assert.strictEqual(body.lifetime, '30s'); + assert.deepStrictEqual(body.delegates, []); + assert.deepStrictEqual(body.scope, [ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + return true; + } + ) + .reply(200, { + accessToken: 'qwerty345', + expireTime: tomorrow.toISOString(), + }), + ]; + + const source_client = new UserRefreshClient( + 'CLIENT_ID', + 'CLIENT_SECRET', + 'REFRESH_TOKEN' + ); + const impersonated = new Impersonated({ + sourceClient: source_client, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + await impersonated.request({url}); + assert.strictEqual(impersonated.credentials.access_token, 'qwerty345'); + assert.strictEqual( + impersonated.credentials.expiry_date, + tomorrow.getTime() + ); + scopes.forEach(s => s.done()); + }); + it('throws meaningful error when context available', async () => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); From 1836f13e0b95125dfebd99934c8423ea76b2da05 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 08:32:02 -0700 Subject: [PATCH 363/662] chore(main): release 8.1.0 (#1428) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9cf5102..d34d0cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.1.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.0.3...v8.1.0) (2022-06-30) + + +### Features + +* handle impersonated ADC ([#1425](https://github.com/googleapis/google-auth-library-nodejs/issues/1425)) ([835be89](https://github.com/googleapis/google-auth-library-nodejs/commit/835be89687c2dff19f34e8b55645d3d611339e14)) + ## [8.0.3](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.0.2...v8.0.3) (2022-06-17) diff --git a/package.json b/package.json index b53b2391..b01c76bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.0.3", + "version": "8.1.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index a1946a54..24c930e2 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.0.3", + "google-auth-library": "^8.1.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 1462f2c533da22b0a07130b25c41df046d4d713d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 4 Jul 2022 21:03:30 +0200 Subject: [PATCH 364/662] fix(deps): update dependency puppeteer to v15 (#1424) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b01c76bc..b6f53142 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^14.0.0", + "puppeteer": "^15.0.0", "sinon": "^14.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 7bd95492..ec667817 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^8.0.0", - "puppeteer": "^14.0.0" + "puppeteer": "^15.0.0" } } From fea6ef9e80bbf556a1014a9e03f9bbf2f1876335 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 8 Jul 2022 23:02:15 +0200 Subject: [PATCH 365/662] chore(deps): update dependency linkinator to v4 (#1430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [linkinator](https://togithub.com/JustinBeckwith/linkinator) | [`^3.0.3` -> `^4.0.0`](https://renovatebot.com/diffs/npm/linkinator/3.1.0/4.0.0) | [![age](https://badges.renovateapi.com/packages/npm/linkinator/4.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/linkinator/4.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/linkinator/4.0.0/compatibility-slim/3.1.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/linkinator/4.0.0/confidence-slim/3.1.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
JustinBeckwith/linkinator ### [`v4.0.0`](https://togithub.com/JustinBeckwith/linkinator/releases/tag/v4.0.0) [Compare Source](https://togithub.com/JustinBeckwith/linkinator/compare/v3.1.0...v4.0.0) ##### Features - create new release with notes ([#​508](https://togithub.com/JustinBeckwith/linkinator/issues/508)) ([2cab633](https://togithub.com/JustinBeckwith/linkinator/commit/2cab633c9659eb10794a4bac06f8b0acdc3e2c0c)) ##### BREAKING CHANGES - The commits in [#​507](https://togithub.com/JustinBeckwith/linkinator/issues/507) and [#​506](https://togithub.com/JustinBeckwith/linkinator/issues/506) both had breaking changes. They included dropping support for Node.js 12.x and updating the CSV export to be streaming, and to use a new way of writing the CSV file. This is an empty to commit using the `BREAKING CHANGE` format in the commit message to ensure a release is triggered.
--- ### Configuration 📅 **Schedule**: Branch creation - "after 9am and before 3pm" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b6f53142..dcd83f0b 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^5.0.0", "keypair": "^1.0.4", - "linkinator": "^3.0.3", + "linkinator": "^4.0.0", "mocha": "^9.2.2", "mv": "^2.1.1", "ncp": "^2.0.0", From 971741196f8dfd612e727a1d9fa9f3e55171fe3b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 13 Jul 2022 11:12:40 -0700 Subject: [PATCH 366/662] chore(main): release 8.1.1 (#1429) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d34d0cea..bf842ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.1.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.1.0...v8.1.1) (2022-07-08) + + +### Bug Fixes + +* **deps:** update dependency puppeteer to v15 ([#1424](https://github.com/googleapis/google-auth-library-nodejs/issues/1424)) ([1462f2c](https://github.com/googleapis/google-auth-library-nodejs/commit/1462f2c533da22b0a07130b25c41df046d4d713d)) + ## [8.1.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.0.3...v8.1.0) (2022-06-30) diff --git a/package.json b/package.json index dcd83f0b..562f53c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.1.0", + "version": "8.1.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 24c930e2..54d1bb35 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.1.0", + "google-auth-library": "^8.1.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From f483a6847fff7a0aa29176aa8704e61640146586 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 3 Aug 2022 20:58:20 +0200 Subject: [PATCH 367/662] deps: update dependency puppeteer to v16 (#1435) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 562f53c6..99f69bf8 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^15.0.0", + "puppeteer": "^16.0.0", "sinon": "^14.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index ec667817..f79ca832 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^8.0.0", - "puppeteer": "^15.0.0" + "puppeteer": "^16.0.0" } } From ed7ef7a5d1fa6bf5d06bdaab278052fd3930fb7f Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Thu, 11 Aug 2022 16:31:25 -0700 Subject: [PATCH 368/662] feat: adds Pluggable Auth support (#1437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat!: Adding Pluggable Auth Support to ADC (#1419) * feat: Adding Pluggable Auth Support to ADC See go/pluggable-auth-design. Adding classes required for supporting pluggable auth and some functionality. Will add the implementation for running the executable and reading from a cached file in a later pull request. * fix: Correcting copyright year * fix: addressing code review comments * fix: Fixing interface description * fix: Comment typo * fix: Address code review comments * fix: Add comments to ExecutableResponse properties * fix: Addressing code review comments * feat: adding executable and file handling for Pluggable Auth Client (#1431) * feat: adding executable and file handling for pluggable auth client Added PluggableAuthHandler to run user provided executable and read from cached file output + associated tests. * fix: correcting pluggable auth credential source Adding 'executable' object under credential source for pluggable auth options. * fix: code review comments * fix: Fixing output file string variable name and failing tests * fix: code review comments + added readme documentation. * fix: addressing code review * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: addressing code review * fix: fixing test * fix: fixing linter warning * fix: Adding back in readme changes that got removed by owlbot * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> --- .readme-partials.yaml | 131 ++++++- README.md | 131 ++++++- samples/test/externalclient.test.js | 51 +++ src/auth/baseexternalclient.ts | 2 +- src/auth/executable-response.ts | 235 +++++++++++++ src/auth/externalclient.ts | 17 +- src/auth/pluggable-auth-client.ts | 314 +++++++++++++++++ src/auth/pluggable-auth-handler.ts | 208 +++++++++++ src/index.ts | 4 + test/test.executableresponse.ts | 340 ++++++++++++++++++ test/test.externalclient.ts | 52 +++ test/test.pluggableauthclient.ts | 479 ++++++++++++++++++++++++++ test/test.pluggableauthhandler.ts | 517 ++++++++++++++++++++++++++++ 13 files changed, 2476 insertions(+), 5 deletions(-) create mode 100644 src/auth/executable-response.ts create mode 100644 src/auth/pluggable-auth-client.ts create mode 100644 src/auth/pluggable-auth-handler.ts create mode 100644 test/test.executableresponse.ts create mode 100644 test/test.pluggableauthclient.ts create mode 100644 test/test.pluggableauthhandler.ts diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 1d48f317..5c995155 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -476,8 +476,137 @@ body: |- - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. - You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC provider. + #### Using Executable-sourced credentials with OIDC and SAML + + **Executable-sourced credentials** + For executable-sourced credentials, a local executable is used to retrieve the 3rd party token. + The executable must handle providing a valid, unexpired OIDC ID token or SAML assertion in JSON format + to stdout. + + To use executable-sourced credentials, the `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES` + environment variable must be set to `1`. + + To generate an executable-sourced workload identity configuration, run the following command: + ```bash + # Generate a configuration file for executable-sourced credentials. + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \ + --service-account=$SERVICE_ACCOUNT_EMAIL \ + --subject-token-type=$SUBJECT_TOKEN_TYPE \ + # The absolute path for the program, including arguments. + # e.g. --executable-command="/path/to/command --foo=bar" + --executable-command=$EXECUTABLE_COMMAND \ + # Optional argument for the executable timeout. Defaults to 30s. + # --executable-timeout-millis=$EXECUTABLE_TIMEOUT \ + # Optional argument for the absolute path to the executable output file. + # See below on how this argument impacts the library behaviour. + # --executable-output-file=$EXECUTABLE_OUTPUT_FILE \ + --output-file /path/to/generated/config.json + ``` + Where the following variables need to be substituted: + - `$PROJECT_NUMBER`: The Google Cloud project number. + - `$POOL_ID`: The workload identity pool ID. + - `$PROVIDER_ID`: The OIDC or SAML provider ID. + - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + - `$SUBJECT_TOKEN_TYPE`: The subject token type. + - `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program. + + The `--executable-timeout-millis` flag is optional. This is the duration for which + the auth library will wait for the executable to finish, in milliseconds. + Defaults to 30 seconds when not provided. The maximum allowed value is 2 minutes. + The minimum is 5 seconds. + + The `--executable-output-file` flag is optional. If provided, the file path must + point to the 3PI credential response generated by the executable. This is useful + for caching the credentials. By specifying this path, the Auth libraries will first + check for its existence before running the executable. By caching the executable JSON + response to this file, it improves performance as it avoids the need to run the executable + until the cached credentials in the output file are expired. The executable must + handle writing to this file - the auth libraries will only attempt to read from + this location. The format of contents in the file should match the JSON format + expected by the executable shown below. + + To retrieve the 3rd party token, the library will call the executable + using the command specified. The executable's output must adhere to the response format + specified below. It must output the response to stdout. + + A sample successful executable OIDC response: + ```json + { + "version": 1, + "success": true, + "token_type": "urn:ietf:params:oauth:token-type:id_token", + "id_token": "HEADER.PAYLOAD.SIGNATURE", + "expiration_time": 1620499962 + } + ``` + + A sample successful executable SAML response: + ```json + { + "version": 1, + "success": true, + "token_type": "urn:ietf:params:oauth:token-type:saml2", + "saml_response": "...", + "expiration_time": 1620499962 + } + ``` + For successful responses, the `expiration_time` field is only required + when an output file is specified in the credential configuration. + + A sample executable error response: + ```json + { + "version": 1, + "success": false, + "code": "401", + "message": "Caller not authorized." + } + ``` + These are all required fields for an error response. The code and message + fields will be used by the library as part of the thrown exception. + + Response format fields summary: + * `version`: The version of the JSON output. Currently, only version 1 is supported. + * `success`: The status of the response. When true, the response must contain the 3rd party token + and token type. The response must also contain the expiration time if an output file was specified in the credential configuration. + The executable must also exit with exit code 0. + When false, the response must contain the error code and message fields and exit with a non-zero value. + * `token_type`: The 3rd party subject token type. Must be *urn:ietf:params:oauth:token-type:jwt*, + *urn:ietf:params:oauth:token-type:id_token*, or *urn:ietf:params:oauth:token-type:saml2*. + * `id_token`: The 3rd party OIDC token. + * `saml_response`: The 3rd party SAML response. + * `expiration_time`: The 3rd party subject token expiration time in seconds (unix epoch time). + * `code`: The error code string. + * `message`: The error message. + + All response types must include both the `version` and `success` fields. + * Successful responses must include the `token_type` and one of + `id_token` or `saml_response`. The `expiration_time` field must also be present if an output file was specified in + the credential configuration. + * Error responses must include both the `code` and `message` fields. + + The library will populate the following environment variables when the executable is run: + * `GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE`: The audience field from the credential configuration. Always present. + * `GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL`: The service account email. Only present when service account impersonation is used. + * `GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE`: The output file location from the credential configuration. Only present when specified in the credential configuration. + * `GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE`: This expected subject token type. Always present. + + These environment variables can be used by the executable to avoid hard-coding these values. + + ##### Security considerations + The following security practices are highly recommended: + * Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script. + * The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion. + + Given the complexity of using executable-sourced credentials, it is recommended to use + the existing supported mechanisms (file-sourced/URL-sourced) for providing 3rd party + credentials unless they do not meet your specific requirements. + + You can now [use the Auth library](#using-external-identities) to call Google Cloud + resources from an OIDC or SAML provider. + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. diff --git a/README.md b/README.md index 91d4139b..059bd944 100644 --- a/README.md +++ b/README.md @@ -520,7 +520,136 @@ Where the following variables need to be substituted: - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. -You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC provider. +#### Using Executable-sourced credentials with OIDC and SAML + +**Executable-sourced credentials** +For executable-sourced credentials, a local executable is used to retrieve the 3rd party token. +The executable must handle providing a valid, unexpired OIDC ID token or SAML assertion in JSON format +to stdout. + +To use executable-sourced credentials, the `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES` +environment variable must be set to `1`. + +To generate an executable-sourced workload identity configuration, run the following command: + +```bash +# Generate a configuration file for executable-sourced credentials. +gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \ + --service-account=$SERVICE_ACCOUNT_EMAIL \ + --subject-token-type=$SUBJECT_TOKEN_TYPE \ + # The absolute path for the program, including arguments. + # e.g. --executable-command="/path/to/command --foo=bar" + --executable-command=$EXECUTABLE_COMMAND \ + # Optional argument for the executable timeout. Defaults to 30s. + # --executable-timeout-millis=$EXECUTABLE_TIMEOUT \ + # Optional argument for the absolute path to the executable output file. + # See below on how this argument impacts the library behaviour. + # --executable-output-file=$EXECUTABLE_OUTPUT_FILE \ + --output-file /path/to/generated/config.json +``` +Where the following variables need to be substituted: +- `$PROJECT_NUMBER`: The Google Cloud project number. +- `$POOL_ID`: The workload identity pool ID. +- `$PROVIDER_ID`: The OIDC or SAML provider ID. +- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. +- `$SUBJECT_TOKEN_TYPE`: The subject token type. +- `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program. + +The `--executable-timeout-millis` flag is optional. This is the duration for which +the auth library will wait for the executable to finish, in milliseconds. +Defaults to 30 seconds when not provided. The maximum allowed value is 2 minutes. +The minimum is 5 seconds. + +The `--executable-output-file` flag is optional. If provided, the file path must +point to the 3PI credential response generated by the executable. This is useful +for caching the credentials. By specifying this path, the Auth libraries will first +check for its existence before running the executable. By caching the executable JSON +response to this file, it improves performance as it avoids the need to run the executable +until the cached credentials in the output file are expired. The executable must +handle writing to this file - the auth libraries will only attempt to read from +this location. The format of contents in the file should match the JSON format +expected by the executable shown below. + +To retrieve the 3rd party token, the library will call the executable +using the command specified. The executable's output must adhere to the response format +specified below. It must output the response to stdout. + +A sample successful executable OIDC response: +```json +{ + "version": 1, + "success": true, + "token_type": "urn:ietf:params:oauth:token-type:id_token", + "id_token": "HEADER.PAYLOAD.SIGNATURE", + "expiration_time": 1620499962 +} +``` + +A sample successful executable SAML response: +```json +{ + "version": 1, + "success": true, + "token_type": "urn:ietf:params:oauth:token-type:saml2", + "saml_response": "...", + "expiration_time": 1620499962 +} +``` +For successful responses, the `expiration_time` field is only required +when an output file is specified in the credential configuration. + +A sample executable error response: +```json +{ + "version": 1, + "success": false, + "code": "401", + "message": "Caller not authorized." +} +``` +These are all required fields for an error response. The code and message +fields will be used by the library as part of the thrown exception. + +Response format fields summary: +* `version`: The version of the JSON output. Currently, only version 1 is supported. +* `success`: The status of the response. When true, the response must contain the 3rd party token + and token type. The response must also contain the expiration time if an output file was specified in the credential configuration. + The executable must also exit with exit code 0. + When false, the response must contain the error code and message fields and exit with a non-zero value. +* `token_type`: The 3rd party subject token type. Must be *urn:ietf:params:oauth:token-type:jwt*, +*urn:ietf:params:oauth:token-type:id_token*, or *urn:ietf:params:oauth:token-type:saml2*. +* `id_token`: The 3rd party OIDC token. +* `saml_response`: The 3rd party SAML response. +* `expiration_time`: The 3rd party subject token expiration time in seconds (unix epoch time). +* `code`: The error code string. +* `message`: The error message. + +All response types must include both the `version` and `success` fields. +* Successful responses must include the `token_type` and one of +`id_token` or `saml_response`. The `expiration_time` field must also be present if an output file was specified in +the credential configuration. +* Error responses must include both the `code` and `message` fields. + +The library will populate the following environment variables when the executable is run: +* `GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE`: The audience field from the credential configuration. Always present. +* `GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL`: The service account email. Only present when service account impersonation is used. +* `GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE`: The output file location from the credential configuration. Only present when specified in the credential configuration. +* `GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE`: This expected subject token type. Always present. + +These environment variables can be used by the executable to avoid hard-coding these values. + +##### Security considerations +The following security practices are highly recommended: +* Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script. +* The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion. + +Given the complexity of using executable-sourced credentials, it is recommended to use +the existing supported mechanisms (file-sourced/URL-sourced) for providing 3rd party +credentials unless they do not meet your specific requirements. + +You can now [use the Auth library](#using-external-identities) to call Google Cloud +resources from an OIDC or SAML provider. ### Using External Identities diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js index 5930fdb2..a9f9023a 100644 --- a/samples/test/externalclient.test.js +++ b/samples/test/externalclient.test.js @@ -217,6 +217,7 @@ describe('samples for external-account', () => { const suffix = generateRandomString(10); const configFilePath = path.join(os.tmpdir(), `config-${suffix}.json`); const oidcTokenFilePath = path.join(os.tmpdir(), `token-${suffix}.txt`); + const executableFilePath = path.join(os.tmpdir(), `executable-${suffix}.sh`); const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform', }); @@ -248,6 +249,9 @@ describe('samples for external-account', () => { if (fs.existsSync(oidcTokenFilePath)) { await unlink(oidcTokenFilePath); } + if (fs.existsSync(executableFilePath)) { + await unlink(executableFilePath); + } // Close any open http servers. if (httpServer) { httpServer.close(); @@ -421,4 +425,51 @@ describe('samples for external-account', () => { // Confirm expected script output. assert.match(output, /DNS Info:/); }); + + it('should acquire ADC for PluggableAuth creds', async () => { + // Create Pluggable Auth configuration JSON file. + const config = { + type: 'external_account', + audience: AUDIENCE_OIDC, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: 'https://sts.googleapis.com/v1/token', + service_account_impersonation_url: + 'https://iamcredentials.googleapis.com/v1/projects/' + + `-/serviceAccounts/${clientEmail}:generateAccessToken`, + credential_source: { + executable: { + command: executableFilePath, + timeout_millis: 5000, + }, + }, + }; + await writeFile(configFilePath, JSON.stringify(config)); + + const expirationTime = Date.now() / 1000 + 60; + const responseJson = { + version: 1, + success: true, + expiration_time: expirationTime, + token_type: 'urn:ietf:params:oauth:token-type:jwt', + id_token: oidcToken, + }; + let exeContent = '#!/bin/bash\n'; + exeContent += 'echo '; + exeContent += JSON.stringify(JSON.stringify(responseJson)); + exeContent += '\n'; + await writeFile(executableFilePath, exeContent, {mode: 0x766}); + // Run sample script with GOOGLE_APPLICATION_CREDENTIALS environment + // variable pointing to the temporarily created configuration file. + const output = await execAsync(`${process.execPath} adc`, { + env: { + ...process.env, + // Set environment variable to allow pluggable auth executable to run. + GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES: 1, + // GOOGLE_APPLICATION_CREDENTIALS environment variable used for ADC. + GOOGLE_APPLICATION_CREDENTIALS: configFilePath, + }, + }); + // Confirm expected script output. + assert.match(output, /DNS Info:/); + }); }); diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index bf13d368..87442634 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -128,7 +128,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { public scopes?: string | string[]; private cachedAccessToken: CredentialsWithResponse | null; protected readonly audience: string; - private readonly subjectTokenType: string; + protected readonly subjectTokenType: string; private readonly serviceAccountImpersonationUrl?: string; private readonly stsCredential: sts.StsCredentials; private readonly clientAuth?: ClientAuthentication; diff --git a/src/auth/executable-response.ts b/src/auth/executable-response.ts new file mode 100644 index 00000000..db4acf56 --- /dev/null +++ b/src/auth/executable-response.ts @@ -0,0 +1,235 @@ +// Copyright 2022 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. + +const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2'; +const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token'; +const OIDC_SUBJECT_TOKEN_TYPE2 = 'urn:ietf:params:oauth:token-type:jwt'; + +/** + * Interface defining the JSON formatted response of a 3rd party executable + * used by the pluggable auth client. + */ +export interface ExecutableResponseJson { + /** + * The version of the JSON response. Only version 1 is currently supported. + * Always required. + */ + version: number; + /** + * Whether the executable ran successfully. Always required. + */ + success: boolean; + /** + * The epoch time for expiration of the token in seconds, required for + * successful responses. + */ + expiration_time?: number; + /** + * The type of subject token in the response, currently supported values are: + * urn:ietf:params:oauth:token-type:saml2 + * urn:ietf:params:oauth:token-type:id_token + * urn:ietf:params:oauth:token-type:jwt + */ + token_type?: string; + /** + * The error code from the executable, required when unsuccessful. + */ + code?: string; + /** + * The error message from the executable, required when unsuccessful. + */ + message?: string; + /** + * The ID token to be used as a subject token when token_type is id_token or jwt. + */ + id_token?: string; + /** + * The response to be used as a subject token when token_type is saml2. + */ + saml_response?: string; +} + +/** + * Defines the response of a 3rd party executable run by the pluggable auth client. + */ +export class ExecutableResponse { + /** + * The version of the Executable response. Only version 1 is currently supported. + */ + readonly version: number; + /** + * Whether the executable ran successfully. + */ + readonly success: boolean; + /** + * The epoch time for expiration of the token in seconds. + */ + readonly expirationTime?: number; + /** + * The type of subject token in the response, currently supported values are: + * urn:ietf:params:oauth:token-type:saml2 + * urn:ietf:params:oauth:token-type:id_token + * urn:ietf:params:oauth:token-type:jwt + */ + readonly tokenType?: string; + /** + * The error code from the executable. + */ + readonly errorCode?: string; + /** + * The error message from the executable. + */ + readonly errorMessage?: string; + /** + * The subject token from the executable, format depends on tokenType. + */ + readonly subjectToken?: string; + + /** + * Instantiates an ExecutableResponse instance using the provided JSON object + * from the output of the executable. + * @param responseJson Response from a 3rd party executable, loaded from a + * run of the executable or a cached output file. + */ + constructor(responseJson: ExecutableResponseJson) { + // Check that the required fields exist in the json response. + if (!responseJson.version) { + throw new InvalidVersionFieldError( + "Executable response must contain a 'version' field." + ); + } + if (responseJson.success === undefined) { + throw new InvalidSuccessFieldError( + "Executable response must contain a 'success' field." + ); + } + + this.version = responseJson.version; + this.success = responseJson.success; + + // Validate required fields for a successful response. + if (this.success) { + this.expirationTime = responseJson.expiration_time; + this.tokenType = responseJson.token_type; + + // Validate token type field. + if ( + this.tokenType !== SAML_SUBJECT_TOKEN_TYPE && + this.tokenType !== OIDC_SUBJECT_TOKEN_TYPE1 && + this.tokenType !== OIDC_SUBJECT_TOKEN_TYPE2 + ) { + throw new InvalidTokenTypeFieldError( + "Executable response must contain a 'token_type' field when successful " + + `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.` + ); + } + + // Validate subject token. + if (this.tokenType === SAML_SUBJECT_TOKEN_TYPE) { + if (!responseJson.saml_response) { + throw new InvalidSubjectTokenError( + `Executable response must contain a 'saml_response' field when token_type=${SAML_SUBJECT_TOKEN_TYPE}.` + ); + } + this.subjectToken = responseJson.saml_response; + } else { + if (!responseJson.id_token) { + throw new InvalidSubjectTokenError( + "Executable response must contain a 'id_token' field when " + + `token_type=${OIDC_SUBJECT_TOKEN_TYPE1} or ${OIDC_SUBJECT_TOKEN_TYPE2}.` + ); + } + this.subjectToken = responseJson.id_token; + } + } else { + // Both code and message must be provided for unsuccessful responses. + if (!responseJson.code) { + throw new InvalidCodeFieldError( + "Executable response must contain a 'code' field when unsuccessful." + ); + } + if (!responseJson.message) { + throw new InvalidMessageFieldError( + "Executable response must contain a 'message' field when unsuccessful." + ); + } + this.errorCode = responseJson.code; + this.errorMessage = responseJson.message; + } + } + + /** + * @return A boolean representing if the response has a valid token. Returns + * true when the response was successful and the token is not expired. + */ + isValid(): boolean { + return !this.isExpired() && this.success; + } + + /** + * @return A boolean representing if the response is expired. Returns true if the + * provided timeout has passed. + */ + isExpired(): boolean { + return ( + this.expirationTime !== undefined && + this.expirationTime < Math.round(Date.now() / 1000) + ); + } +} + +/** + * An error thrown by the ExecutableResponse class. + */ +export class ExecutableResponseError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +/** + * An error thrown when the 'version' field in an executable response is missing or invalid. + */ +export class InvalidVersionFieldError extends ExecutableResponseError {} + +/** + * An error thrown when the 'success' field in an executable response is missing or invalid. + */ +export class InvalidSuccessFieldError extends ExecutableResponseError {} + +/** + * An error thrown when the 'expiration_time' field in an executable response is missing or invalid. + */ +export class InvalidExpirationTimeFieldError extends ExecutableResponseError {} + +/** + * An error thrown when the 'token_type' field in an executable response is missing or invalid. + */ +export class InvalidTokenTypeFieldError extends ExecutableResponseError {} + +/** + * An error thrown when the 'code' field in an executable response is missing or invalid. + */ +export class InvalidCodeFieldError extends ExecutableResponseError {} + +/** + * An error thrown when the 'message' field in an executable response is missing or invalid. + */ +export class InvalidMessageFieldError extends ExecutableResponseError {} + +/** + * An error thrown when the subject token in an executable response is missing or invalid. + */ +export class InvalidSubjectTokenError extends ExecutableResponseError {} diff --git a/src/auth/externalclient.ts b/src/auth/externalclient.ts index 191c6953..b9c792ff 100644 --- a/src/auth/externalclient.ts +++ b/src/auth/externalclient.ts @@ -29,10 +29,15 @@ import { IdentityPoolClientOptions, } from './identitypoolclient'; import {AwsClient, AwsClientOptions} from './awsclient'; +import { + PluggableAuthClient, + PluggableAuthClientOptions, +} from './pluggable-auth-client'; export type ExternalAccountClientOptions = | IdentityPoolClientOptions - | AwsClientOptions; + | AwsClientOptions + | PluggableAuthClientOptions; /** * Dummy class with no constructor. Developers are expected to use fromJSON. @@ -43,7 +48,8 @@ export class ExternalAccountClient { 'ExternalAccountClients should be initialized via: ' + 'ExternalAccountClient.fromJSON(), ' + 'directly via explicit constructors, eg. ' + - 'new AwsClient(options), new IdentityPoolClient(options) or via ' + + 'new AwsClient(options), new IdentityPoolClient(options), new' + + 'PluggableAuthClientOptions, or via ' + 'new GoogleAuth(options).getClient()' ); } @@ -67,6 +73,13 @@ export class ExternalAccountClient { if (options && options.type === EXTERNAL_ACCOUNT_TYPE) { if ((options as AwsClientOptions).credential_source?.environment_id) { return new AwsClient(options as AwsClientOptions, additionalOptions); + } else if ( + (options as PluggableAuthClientOptions).credential_source?.executable + ) { + return new PluggableAuthClient( + options as PluggableAuthClientOptions, + additionalOptions + ); } else { return new IdentityPoolClient( options as IdentityPoolClientOptions, diff --git a/src/auth/pluggable-auth-client.ts b/src/auth/pluggable-auth-client.ts new file mode 100644 index 00000000..d7be6ca8 --- /dev/null +++ b/src/auth/pluggable-auth-client.ts @@ -0,0 +1,314 @@ +// Copyright 2022 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. + +import { + BaseExternalAccountClient, + BaseExternalAccountClientOptions, +} from './baseexternalclient'; +import {RefreshOptions} from './oauth2client'; +import { + ExecutableResponse, + InvalidExpirationTimeFieldError, +} from './executable-response'; +import {PluggableAuthHandler} from './pluggable-auth-handler'; + +/** + * Defines the credential source portion of the configuration for PluggableAuthClient. + * + *

Command is the only required field. If timeout_millis is not specified, the library will + * default to a 30-second timeout. + * + *

+ * Sample credential source for Pluggable Auth Client:
+ * {
+ *   ...
+ *   "credential_source": {
+ *     "executable": {
+ *       "command": "/path/to/get/credentials.sh --arg1=value1 --arg2=value2",
+ *       "timeout_millis": 5000,
+ *       "output_file": "/path/to/generated/cached/credentials"
+ *     }
+ *   }
+ * }
+ * 
+ */ +export interface PluggableAuthClientOptions + extends BaseExternalAccountClientOptions { + credential_source: { + executable: { + /** + * The command used to retrieve the 3rd party token. + */ + command: string; + /** + * The timeout for executable to run in milliseconds. If none is provided it + * will be set to the default timeout of 30 seconds. + */ + timeout_millis?: number; + /** + * An optional output file location that will be checked for a cached response + * from a previous run of the executable. + */ + output_file?: string; + }; + }; +} + +/** + * Error thrown from the executable run by PluggableAuthClient. + */ +export class ExecutableError extends Error { + /** + * The exit code returned by the executable. + */ + readonly code: string; + + constructor(message: string, code: string) { + super( + `The executable failed with exit code: ${code} and error message: ${message}.` + ); + this.code = code; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +/** + * The default executable timeout when none is provided, in milliseconds. + */ +const DEFAULT_EXECUTABLE_TIMEOUT_MILLIS = 30 * 1000; +/** + * The minimum allowed executable timeout in milliseconds. + */ +const MINIMUM_EXECUTABLE_TIMEOUT_MILLIS = 5 * 1000; +/** + * The maximum allowed executable timeout in milliseconds. + */ +const MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS = 120 * 1000; + +/** + * The environment variable to check to see if executable can be run. + * Value must be set to '1' for the executable to run. + */ +const GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES = + 'GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES'; + +/** + * The maximum currently supported executable version. + */ +const MAXIMUM_EXECUTABLE_VERSION = 1; + +/** + * PluggableAuthClient enables the exchange of workload identity pool external credentials for + * Google access tokens by retrieving 3rd party tokens through a user supplied executable. These + * scripts/executables are completely independent of the Google Cloud Auth libraries. These + * credentials plug into ADC and will call the specified executable to retrieve the 3rd party token + * to be exchanged for a Google access token. + * + *

To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable + * must be set to '1'. This is for security reasons. + * + *

Both OIDC and SAML are supported. The executable must adhere to a specific response format + * defined below. + * + *

The executable must print out the 3rd party token to STDOUT in JSON format. When an + * output_file is specified in the credential configuration, the executable must also handle writing the + * JSON response to this file. + * + *

+ * OIDC response sample:
+ * {
+ *   "version": 1,
+ *   "success": true,
+ *   "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ *   "id_token": "HEADER.PAYLOAD.SIGNATURE",
+ *   "expiration_time": 1620433341
+ * }
+ *
+ * SAML2 response sample:
+ * {
+ *   "version": 1,
+ *   "success": true,
+ *   "token_type": "urn:ietf:params:oauth:token-type:saml2",
+ *   "saml_response": "...",
+ *   "expiration_time": 1620433341
+ * }
+ *
+ * Error response sample:
+ * {
+ *   "version": 1,
+ *   "success": false,
+ *   "code": "401",
+ *   "message": "Error message."
+ * }
+ * 
+ * + *

The "expiration_time" field in the JSON response is only required for successful + * responses when an output file was specified in the credential configuration + * + *

The auth libraries will populate certain environment variables that will be accessible by the + * executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE, + * GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and + * GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE. + * + *

Please see this repositories README for a complete executable request/response specification. + */ +export class PluggableAuthClient extends BaseExternalAccountClient { + /** + * The command used to retrieve the third party token. + */ + private readonly command: string; + /** + * The timeout in milliseconds for running executable, + * set to default if none provided. + */ + private readonly timeoutMillis: number; + /** + * The path to file to check for cached executable response. + */ + private readonly outputFile?: string; + + /** + * Executable and output file handler. + */ + private readonly handler: PluggableAuthHandler; + + /** + * Instantiates a PluggableAuthClient instance using the provided JSON + * object loaded from an external account credentials file. + * An error is thrown if the credential is not a valid pluggable auth credential. + * @param options The external account options object typically loaded from + * the external account JSON credential file. + * @param additionalOptions Optional additional behavior customization + * options. These currently customize expiration threshold time and + * whether to retry on 401/403 API request errors. + */ + constructor( + options: PluggableAuthClientOptions, + additionalOptions?: RefreshOptions + ) { + super(options, additionalOptions); + if (!options.credential_source.executable) { + throw new Error('No valid Pluggable Auth "credential_source" provided.'); + } + this.command = options.credential_source.executable.command; + if (!this.command) { + throw new Error('No valid Pluggable Auth "credential_source" provided.'); + } + // Check if the provided timeout exists and if it is valid. + if (options.credential_source.executable.timeout_millis === undefined) { + this.timeoutMillis = DEFAULT_EXECUTABLE_TIMEOUT_MILLIS; + } else { + this.timeoutMillis = options.credential_source.executable.timeout_millis; + if ( + this.timeoutMillis < MINIMUM_EXECUTABLE_TIMEOUT_MILLIS || + this.timeoutMillis > MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS + ) { + throw new Error( + `Timeout must be between ${MINIMUM_EXECUTABLE_TIMEOUT_MILLIS} and ` + + `${MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS} milliseconds.` + ); + } + } + + this.outputFile = options.credential_source.executable.output_file; + + this.handler = new PluggableAuthHandler({ + command: this.command, + timeoutMillis: this.timeoutMillis, + outputFile: this.outputFile, + }); + } + + /** + * Triggered when an external subject token is needed to be exchanged for a + * GCP access token via GCP STS endpoint. + * This uses the `options.credential_source` object to figure out how + * to retrieve the token using the current environment. In this case, + * this calls a user provided executable which returns the subject token. + * The logic is summarized as: + * 1. Validated that the executable is allowed to run. The + * GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment must be set to + * 1 for security reasons. + * 2. If an output file is specified by the user, check the file location + * for a response. If the file exists and contains a valid response, + * return the subject token from the file. + * 3. Call the provided executable and return response. + * @return A promise that resolves with the external subject token. + */ + async retrieveSubjectToken(): Promise { + // Check if the executable is allowed to run. + if (process.env[GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES] !== '1') { + throw new Error( + 'Pluggable Auth executables need to be explicitly allowed to run by ' + + 'setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment ' + + 'Variable to 1.' + ); + } + + let executableResponse: ExecutableResponse | undefined = undefined; + // Try to get cached executable response from output file. + if (this.outputFile) { + executableResponse = await this.handler.retrieveCachedResponse(); + } + // If no response from output file, call the executable. + if (!executableResponse) { + // Set up environment map with required values for the executable. + const envMap = new Map(); + envMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', this.audience); + envMap.set('GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', this.subjectTokenType); + // Always set to 0 because interactive mode is not supported. + envMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); + if (this.outputFile) { + envMap.set('GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE', this.outputFile); + } + const serviceAccountEmail = this.getServiceAccountEmail(); + if (serviceAccountEmail) { + envMap.set( + 'GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL', + serviceAccountEmail + ); + } + executableResponse = await this.handler.retrieveResponseFromExecutable( + envMap + ); + } + + if (executableResponse.version > MAXIMUM_EXECUTABLE_VERSION) { + throw new Error( + `Version of executable is not currently supported, maximum supported version is ${MAXIMUM_EXECUTABLE_VERSION}.` + ); + } + // Check that response was successful. + if (!executableResponse.success) { + throw new ExecutableError( + executableResponse.errorMessage as string, + executableResponse.errorCode as string + ); + } + // Check that response contains expiration time if output file was specified. + if (this.outputFile) { + if (!executableResponse.expirationTime) { + throw new InvalidExpirationTimeFieldError( + 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.' + ); + } + } + // Check that response is not expired. + if (executableResponse.isExpired()) { + throw new Error('Executable response is expired.'); + } + // Return subject token from response. + return executableResponse.subjectToken as string; + } +} diff --git a/src/auth/pluggable-auth-handler.ts b/src/auth/pluggable-auth-handler.ts new file mode 100644 index 00000000..29dc2e17 --- /dev/null +++ b/src/auth/pluggable-auth-handler.ts @@ -0,0 +1,208 @@ +// Copyright 2022 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. + +import {ExecutableError} from './pluggable-auth-client'; +import { + ExecutableResponse, + ExecutableResponseError, + ExecutableResponseJson, +} from './executable-response'; +import * as childProcess from 'child_process'; +import * as fs from 'fs'; + +/** + * Defines the options used for the PluggableAuthHandler class. + */ +export interface PluggableAuthHandlerOptions { + /** + * The command used to retrieve the third party token. + */ + command: string; + /** + * The timeout in milliseconds for running executable, + * set to default if none provided. + */ + timeoutMillis: number; + /** + * The path to file to check for cached executable response. + */ + outputFile?: string; +} + +/** + * A handler used to retrieve 3rd party token responses from user defined + * executables and cached file output for the PluggableAuthClient class. + */ +export class PluggableAuthHandler { + private readonly commandComponents: Array; + private readonly timeoutMillis: number; + private readonly outputFile?: string; + + /** + * Instantiates a PluggableAuthHandler instance using the provided + * PluggableAuthHandlerOptions object. + */ + constructor(options: PluggableAuthHandlerOptions) { + if (!options.command) { + throw new Error('No command provided.'); + } + this.commandComponents = PluggableAuthHandler.parseCommand( + options.command + ) as Array; + this.timeoutMillis = options.timeoutMillis; + if (!this.timeoutMillis) { + throw new Error('No timeoutMillis provided.'); + } + this.outputFile = options.outputFile; + } + + /** + * Calls user provided executable to get a 3rd party subject token and + * returns the response. + * @param envMap a Map of additional Environment Variables required for + * the executable. + * @return A promise that resolves with the executable response. + */ + retrieveResponseFromExecutable( + envMap: Map + ): Promise { + return new Promise((resolve, reject) => { + // Spawn process to run executable using added environment variables. + const child = childProcess.spawn( + this.commandComponents[0], + this.commandComponents.slice(1), + { + env: {...process.env, ...Object.fromEntries(envMap)}, + } + ); + let output = ''; + // Append stdout to output as executable runs. + child.stdout.on('data', (data: string) => { + output += data; + }); + // Append stderr as executable runs. + child.stderr.on('data', (err: string) => { + output += err; + }); + + // Set up a timeout to end the child process and throw an error. + const timeout = setTimeout(() => { + // Kill child process and remove listeners so 'close' event doesn't get + // read after child process is killed. + child.removeAllListeners(); + child.kill(); + return reject( + new Error( + 'The executable failed to finish within the timeout specified.' + ) + ); + }, this.timeoutMillis); + + child.on('close', (code: number) => { + // Cancel timeout if executable closes before timeout is reached. + clearTimeout(timeout); + if (code === 0) { + // If the executable completed successfully, try to return the parsed response. + try { + const responseJson = JSON.parse(output) as ExecutableResponseJson; + const response = new ExecutableResponse(responseJson); + return resolve(response); + } catch (error) { + if (error instanceof ExecutableResponseError) { + return reject(error); + } + return reject( + new ExecutableResponseError( + `The executable returned an invalid response: ${output}` + ) + ); + } + } else { + return reject(new ExecutableError(output, code.toString())); + } + }); + }); + } + + /** + * Checks user provided output file for response from previous run of + * executable and return the response if it exists, is formatted correctly, and is not expired. + */ + async retrieveCachedResponse(): Promise { + if (!this.outputFile || this.outputFile.length === 0) { + return undefined; + } + + let filePath: fs.PathLike; + try { + filePath = await fs.promises.realpath(this.outputFile as string); + } catch { + // If file path cannot be resolved, return undefined. + return undefined; + } + + if (!(await fs.promises.lstat(filePath)).isFile()) { + // If path does not lead to file, return undefined. + return undefined; + } + + const responseString = await fs.promises.readFile(filePath, { + encoding: 'utf8', + }); + + if (responseString === '') { + return undefined; + } + + try { + const responseJson = JSON.parse(responseString) as ExecutableResponseJson; + const response = new ExecutableResponse(responseJson); + + // Check if response is successful and unexpired. + if (response.isValid()) { + return new ExecutableResponse(responseJson); + } + return undefined; + } catch (error) { + if (error instanceof ExecutableResponseError) { + throw error; + } + throw new ExecutableResponseError( + `The output file contained an invalid response: ${responseString}` + ); + } + } + + /** + * Parses given command string into component array, splitting on spaces unless + * spaces are between quotation marks. + */ + private static parseCommand(command: string): Array { + // Split the command into components by splitting on spaces, + // unless spaces are contained in quotation marks. + const components = command.match(/(?:[^\s"]+|"[^"]*")+/g); + if (!components) { + throw new Error(`Provided command: "${command}" could not be parsed.`); + } + + // Remove quotation marks from the beginning and end of each component if they are present. + for (let i = 0; i < components.length; i++) { + if (components[i][0] === '"' && components[i].slice(-1) === '"') { + components[i] = components[i].slice(1, -1); + } + } + + return components; + } +} diff --git a/src/index.ts b/src/index.ts index 4e1f0ead..0139d9fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,6 +62,10 @@ export { CredentialAccessBoundary, DownscopedClient, } from './auth/downscopedclient'; +export { + PluggableAuthClient, + PluggableAuthClientOptions, +} from './auth/pluggable-auth-client'; export {DefaultTransporter} from './transporters'; const auth = new GoogleAuth(); diff --git a/test/test.executableresponse.ts b/test/test.executableresponse.ts new file mode 100644 index 00000000..47705dc4 --- /dev/null +++ b/test/test.executableresponse.ts @@ -0,0 +1,340 @@ +// Copyright 2022 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. + +import * as assert from 'assert'; +import {describe, it} from 'mocha'; +import { + ExecutableResponse, + InvalidSubjectTokenError, + InvalidTokenTypeFieldError, + InvalidCodeFieldError, + InvalidMessageFieldError, + InvalidSuccessFieldError, + InvalidVersionFieldError, +} from '../src/auth/executable-response'; +import * as sinon from 'sinon'; + +const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2'; +const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token'; +const OIDC_SUBJECT_TOKEN_TYPE2 = 'urn:ietf:params:oauth:token-type:jwt'; + +describe('ExecutableResponse', () => { + let clock: sinon.SinonFakeTimers; + const referenceTime = 1653429377000; + + beforeEach(() => { + clock = sinon.useFakeTimers({now: referenceTime}); + }); + + afterEach(() => { + if (clock) { + clock.restore(); + } + }); + + describe('Constructor', () => { + it('should throw error when version field is missing', () => { + const responseJson = { + success: 'true', + }; + const expectedError = new InvalidVersionFieldError( + "Executable response must contain a 'version' field." + ); + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new ExecutableResponse(responseJson); + }, expectedError); + }); + + it('should throw error when success field is missing', () => { + const responseJson = { + version: 1, + }; + const expectedError = new InvalidSuccessFieldError( + "Executable response must contain a 'success' field." + ); + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new ExecutableResponse(responseJson); + }, expectedError); + }); + + it('should throw error when token_type field is missing and success = true', () => { + const responseJson = { + success: true, + version: 1, + expiration_time: 123456, + }; + const expectedError = new InvalidTokenTypeFieldError( + "Executable response must contain a 'token_type' field when successful " + + `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.` + ); + + assert.throws(() => { + return new ExecutableResponse(responseJson); + }, expectedError); + }); + + it('should throw error when token_type field is invalid and success = true', () => { + const responseJson = { + success: true, + version: 1, + expiration_time: 123456, + token_type: 'invalidExample', + }; + const expectedError = new InvalidTokenTypeFieldError( + "Executable response must contain a 'token_type' field when successful " + + `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.` + ); + + assert.throws(() => { + return new ExecutableResponse(responseJson); + }, expectedError); + }); + + it('should throw error when id_token field is missing and token_type is OIDC', () => { + const responseJson = { + success: true, + version: 1, + expiration_time: 123456, + token_type: OIDC_SUBJECT_TOKEN_TYPE1, + saml_response: 'response', + }; + const expectedError = new InvalidSubjectTokenError( + "Executable response must contain a 'id_token' field when token_type=urn:ietf:params:oauth:token-type:id_token or urn:ietf:params:oauth:token-type:jwt." + ); + + assert.throws(() => { + return new ExecutableResponse(responseJson); + }, expectedError); + }); + + it('should throw error when saml_response field is missing and token_type is SAML', () => { + const responseJson = { + success: true, + version: 1, + expiration_time: 123456, + token_type: SAML_SUBJECT_TOKEN_TYPE, + id_token: 'response', + }; + const expectedError = new InvalidSubjectTokenError( + "Executable response must contain a 'saml_response' field when token_type=urn:ietf:params:oauth:token-type:saml2." + ); + + assert.throws(() => { + return new ExecutableResponse(responseJson); + }, expectedError); + }); + + it('should throw error when code field is missing and success is false', () => { + const responseJson = { + success: false, + version: 1, + message: 'error message', + }; + const expectedError = new InvalidCodeFieldError( + "Executable response must contain a 'code' field when unsuccessful." + ); + + assert.throws(() => { + return new ExecutableResponse(responseJson); + }, expectedError); + }); + + it('should throw error when message field is missing and success is false', () => { + const responseJson = { + success: false, + version: 1, + code: '1', + }; + const expectedError = new InvalidMessageFieldError( + "Executable response must contain a 'message' field when unsuccessful." + ); + + assert.throws(() => { + return new ExecutableResponse(responseJson); + }, expectedError); + }); + + it('should should set properties correctly for a successful response with saml', () => { + const responseJson = { + success: true, + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + expiration_time: 123456, + saml_response: 'response', + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(executableResponse.success, responseJson.success); + assert.equal(executableResponse.version, responseJson.version); + assert.equal(executableResponse.tokenType, responseJson.token_type); + assert.equal( + executableResponse.expirationTime, + responseJson.expiration_time + ); + assert.equal(executableResponse.subjectToken, responseJson.saml_response); + }); + + it('should should set properties correctly for a successful response with OIDC type 1', () => { + const responseJson = { + success: true, + version: 1, + token_type: OIDC_SUBJECT_TOKEN_TYPE1, + expiration_time: 123456, + id_token: 'response', + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(executableResponse.success, responseJson.success); + assert.equal(executableResponse.version, responseJson.version); + assert.equal(executableResponse.tokenType, responseJson.token_type); + assert.equal( + executableResponse.expirationTime, + responseJson.expiration_time + ); + assert.equal(executableResponse.subjectToken, responseJson.id_token); + }); + + it('should should set properties correctly for a successful response with OIDC type 2', () => { + const responseJson = { + success: true, + version: 1, + token_type: OIDC_SUBJECT_TOKEN_TYPE2, + expiration_time: 123456, + id_token: 'response', + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(executableResponse.success, responseJson.success); + assert.equal(executableResponse.version, responseJson.version); + assert.equal(executableResponse.tokenType, responseJson.token_type); + assert.equal( + executableResponse.expirationTime, + responseJson.expiration_time + ); + assert.equal(executableResponse.subjectToken, responseJson.id_token); + }); + + it('should should set properties correctly for unsuccessful response', () => { + const responseJson = { + success: false, + version: 1, + code: '1', + message: 'error message', + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(executableResponse.success, responseJson.success); + assert.equal(executableResponse.version, responseJson.version); + assert.equal(executableResponse.errorCode, responseJson.code); + assert.equal(executableResponse.errorMessage, responseJson.message); + }); + }); + + describe('isExpired', () => { + it('should return false if response does not contain expirationTime', () => { + const responseJson = { + success: true, + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 + 1, + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(false, executableResponse.isExpired()); + }); + + it('should return true if response is expired', () => { + const responseJson = { + success: true, + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 - 1, + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(true, executableResponse.isExpired()); + }); + + it('should return false if response is not expired', () => { + const responseJson = { + success: true, + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 + 1, + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(false, executableResponse.isExpired()); + }); + }); + + describe('isValid', () => { + it('should return false if response is not successful', () => { + const responseJson = { + success: false, + version: 1, + code: '1', + message: 'error message', + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(false, executableResponse.isValid()); + }); + + it('should return false if response is successful but expired', () => { + const responseJson = { + success: true, + version: 1, + expiration_time: referenceTime / 1000 - 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(false, executableResponse.isValid()); + }); + + it('should return true if response is successful and not expired', () => { + const responseJson = { + success: true, + version: 1, + expiration_time: referenceTime / 1000 + 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + }; + + const executableResponse = new ExecutableResponse(responseJson); + + assert.equal(true, executableResponse.isValid()); + }); + }); +}); diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index 645ac30c..4a083dff 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -18,6 +18,7 @@ import {AwsClient} from '../src/auth/awsclient'; import {IdentityPoolClient} from '../src/auth/identitypoolclient'; import {ExternalAccountClient} from '../src/auth/externalclient'; import {getAudience, getTokenUrl} from './externalclienthelper'; +import {PluggableAuthClient} from '../src/auth/pluggable-auth-client'; const serviceAccountKeys = { type: 'service_account', @@ -62,6 +63,21 @@ const awsOptions = { credential_source: awsCredentialSource, }; +const pluggableAuthCredentialSource = { + executable: { + command: 'exampleCommand', + timeout_millis: 30000, + output_file: 'output.txt', + }, +}; +const pluggableAuthClientOptions = { + type: 'external_account', + audience: getAudience(), + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: pluggableAuthCredentialSource, +}; + describe('ExternalAccountClient', () => { describe('Constructor', () => { it('should throw on initialization', () => { @@ -159,6 +175,32 @@ describe('ExternalAccountClient', () => { } }); + it('should return PluggableAuthClient on PluggableAuthClientOptions', () => { + const expectedClient = new PluggableAuthClient( + pluggableAuthClientOptions + ); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(pluggableAuthClientOptions), + expectedClient + ); + }); + + it('should return PluggableAuthClient with expected RefreshOptions', () => { + const expectedClient = new PluggableAuthClient( + pluggableAuthClientOptions, + refreshOptions + ); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON( + pluggableAuthClientOptions, + refreshOptions + ), + expectedClient + ); + }); + invalidWorkforceIdentityPoolClientAudiences.forEach( invalidWorkforceIdentityPoolClientAudience => { const workforceIdentityPoolClientInvalidOptions = Object.assign( @@ -217,5 +259,15 @@ describe('ExternalAccountClient', () => { return ExternalAccountClient.fromJSON(invalidOptions); }); }); + + it('should throw when given invalid PluggableAuthClientOptions', () => { + const invalidOptions = Object.assign({}, pluggableAuthClientOptions); + invalidOptions.credential_source.executable.command = 'command'; + invalidOptions.credential_source.executable.timeout_millis = -1; + + assert.throws(() => { + return ExternalAccountClient.fromJSON(invalidOptions); + }); + }); }); }); diff --git a/test/test.pluggableauthclient.ts b/test/test.pluggableauthclient.ts new file mode 100644 index 00000000..aea51776 --- /dev/null +++ b/test/test.pluggableauthclient.ts @@ -0,0 +1,479 @@ +// Copyright 2022 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. + +import * as assert from 'assert'; +import { + ExecutableError, + PluggableAuthClient, +} from '../src/auth/pluggable-auth-client'; +import {BaseExternalAccountClient} from '../src'; +import { + getAudience, + getServiceAccountImpersonationUrl, + getTokenUrl, + saEmail, +} from './externalclienthelper'; +import {beforeEach} from 'mocha'; +import * as sinon from 'sinon'; +import { + ExecutableResponse, + ExecutableResponseJson, + InvalidExpirationTimeFieldError, +} from '../src/auth/executable-response'; +import {PluggableAuthHandler} from '../src/auth/pluggable-auth-handler'; + +const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token'; +const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2'; + +describe('PluggableAuthClient', () => { + const audience = getAudience(); + const pluggableAuthCredentialSource = { + executable: { + command: './command -opt', + output_file: 'output.txt', + timeout_millis: 10000, + }, + }; + const pluggableAuthOptions = { + type: 'external_account', + audience, + subject_token_type: SAML_SUBJECT_TOKEN_TYPE, + token_url: getTokenUrl(), + credential_source: pluggableAuthCredentialSource, + }; + const pluggableAuthOptionsOIDC = { + type: 'external_account', + audience, + subject_token_type: OIDC_SUBJECT_TOKEN_TYPE1, + token_url: getTokenUrl(), + credential_source: pluggableAuthCredentialSource, + }; + const pluggableAuthOptionsWithSA = Object.assign( + { + service_account_impersonation_url: getServiceAccountImpersonationUrl(), + }, + pluggableAuthOptions + ); + const pluggableAuthCredentialSourceNoOutput = { + executable: { + command: './command -opt', + timeout_millis: 10000, + }, + }; + const pluggableAuthOptionsNoOutput = { + type: 'external_account', + audience, + subject_token_type: SAML_SUBJECT_TOKEN_TYPE, + token_url: getTokenUrl(), + credential_source: pluggableAuthCredentialSourceNoOutput, + }; + const pluggableAuthCredentialSourceNoTimeout = { + executable: { + command: './command -opt', + output_file: 'output.txt', + }, + }; + const pluggableAuthOptionsNoTimeout = { + type: 'external_account', + audience, + subject_token_type: SAML_SUBJECT_TOKEN_TYPE, + token_url: getTokenUrl(), + credential_source: pluggableAuthCredentialSourceNoTimeout, + }; + + it('should be a subclass of ExternalAccountClient', () => { + assert(PluggableAuthClient.prototype instanceof BaseExternalAccountClient); + }); + + describe('Constructor', () => { + it('should throw when credential_source is missing executable', () => { + const expectedError = new Error( + 'No valid Pluggable Auth "credential_source" provided.' + ); + const invalidCredentialSource = {}; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new PluggableAuthClient(invalidOptions); + }, expectedError); + }); + + it('should throw when credential_source is missing command', () => { + const expectedError = new Error( + 'No valid Pluggable Auth "credential_source" provided.' + ); + const invalidCredentialSource = { + executable: { + output_file: 'output.txt', + timeout_mills: 10000, + }, + }; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new PluggableAuthClient(invalidOptions); + }, expectedError); + }); + + it('should throw when time_millis is below minimum allowed value', () => { + const expectedError = new Error( + 'Timeout must be between 5000 and 120000 milliseconds.' + ); + const invalidCredentialSource = { + executable: { + command: './command', + output_file: 'output.txt', + timeout_millis: -1, + }, + }; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new PluggableAuthClient(invalidOptions); + }, expectedError); + }); + + it('should throw when time_millis is above maximum allowed value', () => { + const expectedError = new Error( + 'Timeout must be between 5000 and 120000 milliseconds.' + ); + const invalidCredentialSource = { + executable: { + command: './command', + output_file: 'output.txt', + timeout_millis: 9000000000, + }, + }; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new PluggableAuthClient(invalidOptions); + }, expectedError); + }); + + it('should set timeout to default when none is provided', () => { + const client = new PluggableAuthClient(pluggableAuthOptionsNoTimeout); + + assert.equal(client['timeoutMillis'], 30000); + }); + }); + + describe('RetrieveSubjectToken', () => { + const sandbox = sinon.createSandbox(); + let clock: sinon.SinonFakeTimers; + const referenceTime = Date.now(); + let responseJson: ExecutableResponseJson; + let fileStub: sinon.SinonStub<[], Promise>; + let executableStub: sinon.SinonStub< + [envMap: Map], + Promise + >; + + beforeEach(() => { + // Set Allow Executables environment variables to 1 + const envVars = Object.assign({}, process.env, { + GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES: '1', + }); + sandbox.stub(process, 'env').value(envVars); + clock = sinon.useFakeTimers({now: referenceTime}); + + responseJson = { + success: true, + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 + 10, + } as ExecutableResponseJson; + + fileStub = sandbox.stub( + PluggableAuthHandler.prototype, + 'retrieveCachedResponse' + ); + + executableStub = sandbox.stub( + PluggableAuthHandler.prototype, + 'retrieveResponseFromExecutable' + ); + }); + + afterEach(() => { + sandbox.restore(); + if (clock) { + clock.restore(); + } + }); + + it('should throw when allow executables environment variables is not 1', async () => { + process.env.GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES = '0'; + const expectedError = new Error( + 'Pluggable Auth executables need to be explicitly allowed to run by setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment Variable to 1.' + ); + + const client = new PluggableAuthClient(pluggableAuthOptions); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should return error from child process up the stack', async () => { + const expectedError = new Error('example error'); + fileStub.throws(new Error('example error')); + + const client = new PluggableAuthClient(pluggableAuthOptions); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should return executable SAML response when successful', async () => { + const client = new PluggableAuthClient(pluggableAuthOptions); + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + + assert.equal(await subjectToken, responseJson.saml_response); + }); + + it('should return executable OIDC response when successful', async () => { + const client = new PluggableAuthClient(pluggableAuthOptionsOIDC); + responseJson.id_token = 'subject_token'; + responseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; + responseJson.saml_response = undefined; + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + + assert.equal(await subjectToken, responseJson.id_token); + }); + + it('should return SAML executable response when successful and no expiration_time', async () => { + const client = new PluggableAuthClient(pluggableAuthOptionsNoOutput); + responseJson.expiration_time = undefined; + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + + assert.equal(await subjectToken, responseJson.saml_response); + }); + + it('should return OIDC executable response when successful and no expiration_time', async () => { + const client = new PluggableAuthClient(pluggableAuthOptionsNoOutput); + responseJson.id_token = 'subject_token'; + responseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; + responseJson.saml_response = undefined; + responseJson.expiration_time = undefined; + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + + assert.equal(await subjectToken, responseJson.id_token); + }); + + it('should throw error when version is not supported', async () => { + responseJson.version = 99999; + const expectedError = new Error( + 'Version of executable is not currently supported, maximum supported version is 1.' + ); + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + const client = new PluggableAuthClient(pluggableAuthOptions); + + const subjectToken = client.retrieveSubjectToken(); + + await assert.rejects(subjectToken, expectedError); + }); + + it('should throw error when response is expired', async () => { + responseJson.expiration_time = referenceTime / 1000 - 10; + const expectedError = new Error('Executable response is expired.'); + const client = new PluggableAuthClient(pluggableAuthOptions); + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + + await assert.rejects(subjectToken, expectedError); + }); + + it('should call executable when output file returns undefined', async () => { + const client = new PluggableAuthClient(pluggableAuthOptions); + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + await client.retrieveSubjectToken(); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.calledOnce(executableStub); + }); + + it('should return cached file SAML response when successful', async () => { + const client = new PluggableAuthClient(pluggableAuthOptions); + fileStub.resolves(new ExecutableResponse(responseJson)); + const subjectToken = client.retrieveSubjectToken(); + + assert.equal(await subjectToken, responseJson.saml_response); + }); + + it('should return cached file OIDC response when successful', async () => { + const client = new PluggableAuthClient(pluggableAuthOptionsOIDC); + responseJson.id_token = 'subject_token'; + responseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; + responseJson.saml_response = undefined; + fileStub.resolves(new ExecutableResponse(responseJson)); + const subjectToken = client.retrieveSubjectToken(); + + assert.equal(await subjectToken, responseJson.id_token); + }); + + it('should reject if error returned from output file stream', async () => { + const client = new PluggableAuthClient(pluggableAuthOptions); + const expectedError = new Error('error'); + fileStub.rejects(expectedError); + const subjectToken = client.retrieveSubjectToken(); + + await assert.rejects(subjectToken, expectedError); + }); + + it('should throw error when response is not successful', async () => { + responseJson.success = false; + responseJson.code = '1'; + responseJson.message = 'error'; + const expectedError = new ExecutableError('error', '1'); + const client = new PluggableAuthClient(pluggableAuthOptions); + fileStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + + await assert.rejects(subjectToken, expectedError); + }); + + it('should throw error when output file response does not contain expiration_time and output file is specified', async () => { + responseJson.expiration_time = undefined; + const expectedError = new InvalidExpirationTimeFieldError( + 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.' + ); + const client = new PluggableAuthClient(pluggableAuthOptions); + fileStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + + await assert.rejects(subjectToken, expectedError); + }); + + it('should throw error when executable response does not contain expiration_time and output file is specified', async () => { + responseJson.expiration_time = undefined; + const expectedError = new InvalidExpirationTimeFieldError( + 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.' + ); + const client = new PluggableAuthClient(pluggableAuthOptions); + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + + await assert.rejects(subjectToken, expectedError); + }); + + it('should set envMap correctly when calling executable', async () => { + const expectedEnvMap = new Map(); + expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', audience); + expectedEnvMap.set( + 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', + responseJson.token_type + ); + expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); + const client = new PluggableAuthClient(pluggableAuthOptionsNoOutput); + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + await subjectToken; + + sinon.assert.calledOnceWithExactly(executableStub, expectedEnvMap); + }); + + it('should set envMap correctly when calling executable without output file', async () => { + const expectedEnvMap = new Map(); + expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', audience); + expectedEnvMap.set( + 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', + responseJson.token_type + ); + expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); + expectedEnvMap.set( + 'GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE', + pluggableAuthCredentialSource.executable.output_file + ); + const client = new PluggableAuthClient(pluggableAuthOptions); + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + await subjectToken; + + sinon.assert.calledOnceWithExactly(executableStub, expectedEnvMap); + }); + + it('should set envMap correctly when calling executable with service account impersonation', async () => { + const expectedEnvMap = new Map(); + expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', audience); + expectedEnvMap.set( + 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', + responseJson.token_type + ); + expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); + expectedEnvMap.set( + 'GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE', + pluggableAuthCredentialSource.executable.output_file + ); + expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL', saEmail); + const client = new PluggableAuthClient(pluggableAuthOptionsWithSA); + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + const subjectToken = client.retrieveSubjectToken(); + await subjectToken; + + sinon.assert.calledOnceWithExactly(executableStub, expectedEnvMap); + }); + }); +}); diff --git a/test/test.pluggableauthhandler.ts b/test/test.pluggableauthhandler.ts new file mode 100644 index 00000000..b82710cb --- /dev/null +++ b/test/test.pluggableauthhandler.ts @@ -0,0 +1,517 @@ +// Copyright 2022 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. + +import * as sinon from 'sinon'; +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import { + ExecutableResponse, + ExecutableResponseError, + ExecutableResponseJson, + InvalidSuccessFieldError, +} from '../src/auth/executable-response'; +import {beforeEach} from 'mocha'; +import * as events from 'events'; +import * as stream from 'stream'; +import { + PluggableAuthHandler, + PluggableAuthHandlerOptions, +} from '../src/auth/pluggable-auth-handler'; +import * as assert from 'assert'; +import {ExecutableError} from '../src/auth/pluggable-auth-client'; + +const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token'; +const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2'; + +describe('PluggableAuthHandler', () => { + const defaultHandlerOptions = { + command: './command/path/file.exe -opt', + outputFile: 'output', + timeoutMillis: 1000, + } as PluggableAuthHandlerOptions; + + describe('Constructor', () => { + it('should not throw error with valid options', () => { + const client = new PluggableAuthHandler(defaultHandlerOptions); + + assert(client instanceof PluggableAuthHandler); + }); + + it('should throw when options is missing command', () => { + const expectedError = new Error('No command provided.'); + const invalidOptions = { + outputFile: 'output.txt', + timeoutMillis: 1000, + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new PluggableAuthHandler(invalidOptions); + }, expectedError); + }); + + it('should throw when options is missing timeoutMillis', () => { + const expectedError = new Error('No timeoutMillis provided.'); + const invalidOptions = { + command: './command -opt', + outputFile: 'output.txt', + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new PluggableAuthHandler(invalidOptions); + }, expectedError); + }); + + it('should throw when command cannot be parsed', async () => { + const expectedError = new Error( + 'Provided command: " " could not be parsed.' + ); + const invalidHandlerOptions = { + command: ' ', + timeoutMillis: 10000, + } as PluggableAuthHandlerOptions; + + assert.throws(() => { + return new PluggableAuthHandler(invalidHandlerOptions); + }, expectedError); + }); + }); + + describe('RetrieveResponseFromExecutable', () => { + const sandbox = sinon.createSandbox(); + let clock: sinon.SinonFakeTimers; + const referenceTime = Date.now(); + let spawnEvent: child_process.ChildProcess; + let spawnStub: sinon.SinonStub< + [ + command: string, + args: readonly string[], + options: child_process.SpawnOptions + ], + child_process.ChildProcess + >; + let defaultResponseJson: ExecutableResponseJson; + const expectedEnvMap = new Map(); + expectedEnvMap.set( + 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', + SAML_SUBJECT_TOKEN_TYPE + ); + expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); + const expectedOpts = { + env: { + ...process.env, + ...Object.fromEntries(expectedEnvMap), + }, + }; + + beforeEach(() => { + // Stub environment variables + sandbox.stub(process, 'env').value(process.env); + clock = sandbox.useFakeTimers({now: referenceTime}); + + defaultResponseJson = { + success: true, + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 + 10, + } as ExecutableResponseJson; + + // Stub child_process.spawn to return an event emitter. + spawnEvent = new events.EventEmitter() as child_process.ChildProcess; + spawnEvent.stdout = new events.EventEmitter() as stream.Readable; + spawnEvent.stderr = new events.EventEmitter() as stream.Readable; + spawnStub = sandbox.stub(child_process, 'spawn').returns(spawnEvent); + }); + + afterEach(() => { + sandbox.restore(); + if (clock) { + clock.restore(); + } + }); + + it('should return error from child process up the stack', async () => { + const expectedError = new Error('example error'); + spawnStub.throws(new Error('example error')); + const handler = new PluggableAuthHandler(defaultHandlerOptions); + + await assert.rejects( + handler.retrieveResponseFromExecutable(new Map()), + expectedError + ); + }); + + it('should return SAML executable response when successful', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + + const response = handler.retrieveResponseFromExecutable( + new Map() + ); + spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); + spawnEvent.emit('close', 0); + + assert.deepEqual( + await response, + new ExecutableResponse(defaultResponseJson) + ); + }); + + it('should return OIDC executable response when successful', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + defaultResponseJson.saml_response = undefined; + defaultResponseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; + defaultResponseJson.id_token = 'subject token'; + const response = handler.retrieveResponseFromExecutable( + new Map() + ); + spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); + spawnEvent.emit('close', 0); + + assert.deepEqual( + await response, + new ExecutableResponse(defaultResponseJson) + ); + }); + + it('should return SAML executable response when successful and no expiration_time', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + defaultResponseJson.expiration_time = undefined; + const response = handler.retrieveResponseFromExecutable( + new Map() + ); + spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); + spawnEvent.emit('close', 0); + + assert.deepEqual( + await response, + new ExecutableResponse(defaultResponseJson) + ); + }); + + it('should return OIDC executable response when successful and no expiration_time', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + defaultResponseJson.expiration_time = undefined; + defaultResponseJson.saml_response = undefined; + defaultResponseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; + defaultResponseJson.id_token = 'subject token'; + const response = handler.retrieveResponseFromExecutable( + new Map() + ); + spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); + spawnEvent.emit('close', 0); + + assert.deepEqual( + await response, + new ExecutableResponse(defaultResponseJson) + ); + }); + + it('should call executable with correct command and env variables', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const components = defaultHandlerOptions.command.split(' '); + const expectedCommand = components[0]; + const expectedArgs = components.slice(1); + + const response = handler.retrieveResponseFromExecutable(expectedEnvMap); + spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); + spawnEvent.emit('close', 0); + await response; + + sandbox.assert.calledOnceWithMatch( + spawnStub, + expectedCommand, + expectedArgs, + expectedOpts + ); + }); + + it('should call executable with correct command with spaces', async () => { + const handlerOptions = { + command: '"./command with/spaces.exe" -opt arg', + outputFile: 'output', + timeoutMillis: 1000, + } as PluggableAuthHandlerOptions; + const handler = new PluggableAuthHandler(handlerOptions); + const expectedCommand = './command with/spaces.exe'; + const expectedArgs = ['-opt', 'arg']; + + const response = handler.retrieveResponseFromExecutable(expectedEnvMap); + spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); + spawnEvent.emit('close', 0); + await response; + + sandbox.assert.calledOnceWithMatch( + spawnStub, + expectedCommand, + expectedArgs, + expectedOpts + ); + }); + + it('should call executable with correct arguments with spaces', async () => { + const handlerOptions = { + command: './command/with/path.exe -opt "arg with spaces"', + outputFile: 'output', + timeoutMillis: 1000, + } as PluggableAuthHandlerOptions; + const handler = new PluggableAuthHandler(handlerOptions); + const expectedCommand = './command/with/path.exe'; + const expectedArgs = ['-opt', 'arg with spaces']; + + const response = handler.retrieveResponseFromExecutable(expectedEnvMap); + spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); + spawnEvent.emit('close', 0); + await response; + + sandbox.assert.calledOnceWithMatch( + spawnStub, + expectedCommand, + expectedArgs, + expectedOpts + ); + }); + + it('should throw ExecutableError when executable fails', async () => { + const expectedError = new ExecutableError('test error', '1'); + const handler = new PluggableAuthHandler(defaultHandlerOptions); + + const response = handler.retrieveResponseFromExecutable( + new Map() + ); + spawnEvent.stderr!.emit('data', 'test error'); + spawnEvent.emit('close', 1); + + await assert.rejects(response, expectedError); + }); + + it('should throw error when executable times out', async () => { + const expectedError = new Error( + 'The executable failed to finish within the timeout specified.' + ); + spawnEvent.kill = () => { + return true; + }; + const handler = new PluggableAuthHandler(defaultHandlerOptions); + + const response = handler.retrieveResponseFromExecutable( + new Map() + ); + clock.tick(10001); + + await assert.rejects(response, expectedError); + }); + + it('should throw error when non-json text is returned', async () => { + const expectedError = new ExecutableResponseError( + 'The executable returned an invalid response: THIS_IS_NOT_JSON' + ); + const handler = new PluggableAuthHandler(defaultHandlerOptions); + + const response = handler.retrieveResponseFromExecutable( + new Map() + ); + spawnEvent.stdout!.emit('data', 'THIS_IS_NOT_JSON'); + spawnEvent.emit('close', 0); + + await assert.rejects(response, expectedError); + }); + + it('should throw ExecutableResponseError', async () => { + const expectedError = new InvalidSuccessFieldError( + "Executable response must contain a 'success' field." + ); + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const invalidResponse = { + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 + 10, + }; + + const response = handler.retrieveResponseFromExecutable( + new Map() + ); + spawnEvent.stdout!.emit('data', JSON.stringify(invalidResponse)); + spawnEvent.emit('close', 0); + + await assert.rejects(response, expectedError); + }); + }); + + describe('retrieveCachedResponse', () => { + const sandbox = sinon.createSandbox(); + let clock: sinon.SinonFakeTimers; + const referenceTime = Date.now(); + let realPathStub: sinon.SinonStub< + [ + path: fs.PathLike, + options?: fs.ObjectEncodingOptions | BufferEncoding | null | undefined + ], + Promise + >; + let statStub: sinon.SinonStub< + [path: fs.PathLike, opts?: fs.StatOptions | undefined], + Promise + >; + let defaultResponseJson: ExecutableResponseJson; + + beforeEach(() => { + clock = sandbox.useFakeTimers({now: referenceTime}); + + defaultResponseJson = { + success: true, + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 + 10, + } as ExecutableResponseJson; + + // Stub fs methods, so we don't have to read a real file. + realPathStub = sandbox.stub(fs.promises, 'realpath').returnsArg(0); + const fakeStat = Promise.resolve({isFile: () => true} as fs.Stats); + statStub = sandbox.stub(fs.promises, 'lstat').returns(fakeStat); + }); + + afterEach(() => { + sandbox.restore(); + if (clock) { + clock.restore(); + } + }); + + it('should return cached file SAML response when successful', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const response = handler.retrieveCachedResponse(); + sandbox + .stub(fs.promises, 'readFile') + .resolves(JSON.stringify(defaultResponseJson)); + + assert.deepEqual( + await response, + new ExecutableResponse(defaultResponseJson) + ); + }); + + it('should return cached file OIDC response when successful', async () => { + defaultResponseJson.saml_response = undefined; + defaultResponseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; + defaultResponseJson.id_token = 'subject token'; + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const response = handler.retrieveCachedResponse(); + sandbox + .stub(fs.promises, 'readFile') + .resolves(JSON.stringify(defaultResponseJson)); + + assert.deepEqual( + await response, + new ExecutableResponse(defaultResponseJson) + ); + }); + + it('should reject if error returned from output file stream', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const expectedError = new Error('error'); + const response = handler.retrieveCachedResponse(); + sandbox.stub(fs.promises, 'readFile').rejects(expectedError); + + await assert.rejects(response, expectedError); + }); + + it('should return undefined if file response is expired', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + defaultResponseJson.expiration_time = referenceTime / 1000 - 10; + const response = handler.retrieveCachedResponse(); + sandbox + .stub(fs.promises, 'readFile') + .resolves(JSON.stringify(defaultResponseJson)); + + assert.equal(await response, undefined); + }); + + it('should return undefined if path cannot be resolved', async () => { + realPathStub.throws(new Error('ENOENT')); + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const response = handler.retrieveCachedResponse(); + + assert.equal(await response, undefined); + }); + + it('should return undefined if outputFile is undefined', async () => { + const invalidOptions = { + command: './command.sh', + timeoutMillis: 1000, + }; + const handler = new PluggableAuthHandler(invalidOptions); + const response = handler.retrieveCachedResponse(); + + assert.equal(await response, undefined); + }); + + it('should return undefined if output file does not exist', async () => { + const fakeStat = {isFile: () => false} as fs.Stats; + statStub.resolves(fakeStat); + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const response = handler.retrieveCachedResponse(); + sandbox + .stub(fs.promises, 'readFile') + .resolves(JSON.stringify(defaultResponseJson)); + + assert.equal(await response, undefined); + }); + + it('should return undefined if output file is empty', async () => { + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const response = handler.retrieveCachedResponse(); + sandbox.stub(fs.promises, 'readFile').resolves(''); + + assert.equal(await response, undefined); + }); + + it('should throw error when non-json text is returned', async () => { + const expectedError = new ExecutableResponseError( + 'The output file contained an invalid response: THIS_IS_NOT_JSON' + ); + const handler = new PluggableAuthHandler(defaultHandlerOptions); + + const response = handler.retrieveCachedResponse(); + sandbox.stub(fs.promises, 'readFile').resolves('THIS_IS_NOT_JSON'); + + await assert.rejects(response, expectedError); + }); + + it('should throw ExecutableResponseError', async () => { + const expectedError = new InvalidSuccessFieldError( + "Executable response must contain a 'success' field." + ); + const handler = new PluggableAuthHandler(defaultHandlerOptions); + const invalidResponse = { + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 + 10, + }; + + const response = handler.retrieveCachedResponse(); + sandbox + .stub(fs.promises, 'readFile') + .resolves(JSON.stringify(invalidResponse)); + + await assert.rejects(response, expectedError); + }); + }); +}); From 1aecd1f8d0dbc2451ef0e450e803a5b2b144940f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 14:06:23 -0700 Subject: [PATCH 369/662] chore(main): release 8.2.0 (#1438) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf842ec9..f18c3f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.2.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.1.1...v8.2.0) (2022-08-11) + + +### Features + +* adds Pluggable Auth support ([#1437](https://github.com/googleapis/google-auth-library-nodejs/issues/1437)) ([ed7ef7a](https://github.com/googleapis/google-auth-library-nodejs/commit/ed7ef7a5d1fa6bf5d06bdaab278052fd3930fb7f)) + ## [8.1.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.1.0...v8.1.1) (2022-07-08) diff --git a/package.json b/package.json index 99f69bf8..9c4ae88f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.1.1", + "version": "8.2.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 54d1bb35..78ea6d10 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.1.1", + "google-auth-library": "^8.2.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 4ace981ba1d37a9a00201a1c91e1b79c8cdb5cec Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 19 Aug 2022 08:52:28 -0700 Subject: [PATCH 370/662] feat: Add `generateIdToken` support for `Impersonated` Clients (#1439) * feat: Add `generateIdToken` support for Impersonated Clients * feat: Add `FetchIdTokenOptions` and default `includeEmail` to `true` --- src/auth/impersonated.ts | 47 ++++++++++- test/test.impersonated.ts | 170 ++++++++++++++++++++++++++------------ 2 files changed, 161 insertions(+), 56 deletions(-) diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index 061faec0..b2cb6994 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -16,6 +16,7 @@ import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; import {AuthClient} from './authclient'; +import {IdTokenProvider} from './idtokenclient'; import {GaxiosError} from 'gaxios'; export interface ImpersonatedOptions extends RefreshOptions { @@ -52,7 +53,20 @@ export interface TokenResponse { expireTime: string; } -export class Impersonated extends OAuth2Client { +export interface FetchIdTokenOptions { + /** + * Include the service account email in the token. + * If set to `true`, the token will contain `email` and `email_verified` claims. + */ + includeEmail: boolean; +} + +export interface FetchIdTokenResponse { + /** The OpenId Connect ID token. */ + token: string; +} + +export class Impersonated extends OAuth2Client implements IdTokenProvider { private sourceClient: AuthClient; private targetPrincipal: string; private targetScopes: string[]; @@ -154,4 +168,35 @@ export class Impersonated extends OAuth2Client { } } } + + /** + * Generates an OpenID Connect ID token for a service account. + * + * {@link https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken Reference Documentation} + * + * @param targetAudience the audience for the fetched ID token. + * @param options the for the request + * @return an OpenID Connect ID token + */ + async fetchIdToken( + targetAudience: string, + options?: FetchIdTokenOptions + ): Promise { + await this.sourceClient.getAccessToken(); + + const name = `projects/-/serviceAccounts/${this.targetPrincipal}`; + const u = `${this.endpoint}/v1/${name}:generateIdToken`; + const body = { + delegates: this.delegates, + audience: targetAudience, + includeEmail: options?.includeEmail ?? true, + }; + const res = await this.sourceClient.request({ + url: u, + data: body, + method: 'POST', + }); + + return res.data.token; + } } diff --git a/test/test.impersonated.ts b/test/test.impersonated.ts index 51de9592..8b4886e0 100644 --- a/test/test.impersonated.ts +++ b/test/test.impersonated.ts @@ -32,6 +32,18 @@ function createGTokenMock(body: CredentialRequest) { .reply(200, body); } +function createSampleJWTClient() { + const jwt = new JWT( + 'foo@serviceaccount.com', + PEM_PATH, + undefined, + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com' + ); + + return jwt; +} + interface ImpersonatedCredentialRequest { delegates: string[]; scope: string[]; @@ -68,15 +80,9 @@ describe('impersonated', () => { expireTime: tomorrow.toISOString(), }), ]; - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const impersonated = new Impersonated({ - sourceClient: jwt, + sourceClient: createSampleJWTClient(), targetPrincipal: 'target@project.iam.gserviceaccount.com', lifetime: 30, delegates: [], @@ -117,15 +123,9 @@ describe('impersonated', () => { expireTime: tomorrow.toISOString(), }), ]; - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const impersonated = new Impersonated({ - sourceClient: jwt, + sourceClient: createSampleJWTClient(), targetPrincipal: 'target@project.iam.gserviceaccount.com', lifetime: 30, delegates: [], @@ -176,13 +176,7 @@ describe('impersonated', () => { expireTime: tomorrow.toISOString(), }), ]; - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const jwt = createSampleJWTClient(); const impersonated = new Impersonated({ sourceClient: jwt, targetPrincipal: 'target@project.iam.gserviceaccount.com', @@ -270,15 +264,9 @@ describe('impersonated', () => { }, }), ]; - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const impersonated = new Impersonated({ - sourceClient: jwt, + sourceClient: createSampleJWTClient(), targetPrincipal: 'target@project.iam.gserviceaccount.com', lifetime: 30, delegates: [], @@ -303,15 +291,9 @@ describe('impersonated', () => { ) .reply(500), ]; - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const impersonated = new Impersonated({ - sourceClient: jwt, + sourceClient: createSampleJWTClient(), targetPrincipal: 'target@project.iam.gserviceaccount.com', lifetime: 30, delegates: [], @@ -329,15 +311,9 @@ describe('impersonated', () => { const scopes = [ nock('https://www.googleapis.com').post('/oauth2/v4/token').reply(401), ]; - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const impersonated = new Impersonated({ - sourceClient: jwt, + sourceClient: createSampleJWTClient(), targetPrincipal: 'target@project.iam.gserviceaccount.com', lifetime: 30, delegates: [], @@ -371,15 +347,9 @@ describe('impersonated', () => { expireTime: tomorrow.toISOString(), }), ]; - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const impersonated = new Impersonated({ - sourceClient: jwt, + sourceClient: createSampleJWTClient(), targetPrincipal: 'target@project.iam.gserviceaccount.com', lifetime: 30, delegates: [], @@ -395,4 +365,94 @@ describe('impersonated', () => { ); scopes.forEach(s => s.done()); }); + + it('should fetch an OpenID Connect ID token w/ `includeEmail` by default', async () => { + const expectedToken = 'OpenID-Connect-ID-token'; + const expectedAudience = 'sample-audience'; + const expectedDeligates = ['deligate-1', 'deligate-2']; + const expectedIncludeEmail = true; + + const scopes = [ + createGTokenMock({ + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateIdToken', + (body: { + delegates: string[]; + audience: string; + includeEmail: boolean; + }) => { + assert.strictEqual(body.audience, expectedAudience); + assert.strictEqual(body.includeEmail, expectedIncludeEmail); + assert.deepStrictEqual(body.delegates, expectedDeligates); + return true; + } + ) + .reply(200, { + token: expectedToken, + }), + ]; + + const impersonated = new Impersonated({ + sourceClient: createSampleJWTClient(), + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: expectedDeligates, + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + + const token = await impersonated.fetchIdToken(expectedAudience); + + assert.equal(token, expectedToken); + + scopes.forEach(s => s.done()); + }); + + it('should fetch an OpenID Connect ID token with desired options', async () => { + const expectedToken = 'OpenID-Connect-ID-token'; + const expectedAudience = 'sample-audience'; + const expectedDeligates = ['deligate-1', 'deligate-2']; + const expectedIncludeEmail = false; + + const scopes = [ + createGTokenMock({ + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateIdToken', + (body: { + delegates: string[]; + audience: string; + includeEmail: boolean; + }) => { + assert.strictEqual(body.audience, expectedAudience); + assert.strictEqual(body.includeEmail, expectedIncludeEmail); + assert.deepStrictEqual(body.delegates, expectedDeligates); + return true; + } + ) + .reply(200, { + token: expectedToken, + }), + ]; + + const impersonated = new Impersonated({ + sourceClient: createSampleJWTClient(), + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: expectedDeligates, + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + + const token = await impersonated.fetchIdToken(expectedAudience, { + includeEmail: expectedIncludeEmail, + }); + + assert.equal(token, expectedToken); + + scopes.forEach(s => s.done()); + }); }); From 89b6e63aa948d1716a16366163f311a2aadbf3d5 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:44:13 -0700 Subject: [PATCH 371/662] chore(main): release 8.3.0 (#1440) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f18c3f7f..d4c2af10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.3.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.2.0...v8.3.0) (2022-08-19) + + +### Features + +* Add `generateIdToken` support for `Impersonated` Clients ([#1439](https://github.com/googleapis/google-auth-library-nodejs/issues/1439)) ([4ace981](https://github.com/googleapis/google-auth-library-nodejs/commit/4ace981ba1d37a9a00201a1c91e1b79c8cdb5cec)) + ## [8.2.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.1.1...v8.2.0) (2022-08-11) diff --git a/package.json b/package.json index 9c4ae88f..5a10c938 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.2.0", + "version": "8.3.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 78ea6d10..060ce9a2 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.2.0", + "google-auth-library": "^8.3.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 415108b31860840c7b7d77da5f890c2d5572d9b3 Mon Sep 17 00:00:00 2001 From: Shogo Nakano <61229807+shogo-nakano-desu@users.noreply.github.com> Date: Sat, 20 Aug 2022 03:59:07 +0900 Subject: [PATCH 372/662] refactor: remove comment and promises, instead only using async/await (#1436) Co-authored-by: Benjamin E. Coe Co-authored-by: Daniel Bankhead --- src/auth/googleauth.ts | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index cc7be332..fa8d0cb2 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -203,32 +203,23 @@ export class GoogleAuth { // - 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); - } + this._getDefaultProjectIdPromise = (async () => { + 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' + ); } - ); + return projectId; + })(); } return this._getDefaultProjectIdPromise; } From 7e067327634281bba948c9cc6bc99c7ab860f827 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Fri, 19 Aug 2022 15:32:24 -0700 Subject: [PATCH 373/662] fix(functions): clarify auth comments (#1427) * fix: clarify auth comments * Fix lint Co-authored-by: Benjamin E. Coe Co-authored-by: Daniel Bankhead --- samples/idtokens-serverless.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index 701b91ad..2012c3cb 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -33,16 +33,30 @@ function main( /** * TODO(developer): Uncomment these variables before running the sample. */ + // [END functions_bearer_token] + // [END cloudrun_service_to_service_auth] + + // [START cloudrun_service_to_service_auth] // Example: https://my-cloud-run-service.run.app/books/delete/12345 // const url = 'https://TARGET_HOSTNAME/TARGET_URL'; - // [END functions_bearer_token] // Example (Cloud Run): https://my-cloud-run-service.run.app/ - // [START functions_bearer_token] + // const targetAudience = 'https://TARGET_AUDIENCE/'; // [END cloudrun_service_to_service_auth] + + // [START functions_bearer_token] + // For Cloud Functions, `endpoint` and `audience` should be equal. + + // Example: https://project-region-projectid.cloudfunctions.net/myFunction + // const url = 'https://TARGET_HOSTNAME/TARGET_URL'; + // Example (Cloud Functions): https://project-region-projectid.cloudfunctions.net/myFunction - // [START cloudrun_service_to_service_auth] // const targetAudience = 'https://TARGET_AUDIENCE/'; + // [END functions_bearer_token] + + // [START functions_bearer_token] + // [START cloudrun_service_to_service_auth] + const {GoogleAuth} = require('google-auth-library'); const auth = new GoogleAuth(); From 4436e6c41daf7f3831798270719b83c644deed98 Mon Sep 17 00:00:00 2001 From: Quentin Barbe Date: Sat, 20 Aug 2022 00:56:59 +0200 Subject: [PATCH 374/662] fet(deps): gtoken with support for transporter (#1426) * chore(deps): update gtoken * feat: pass jwt transporter to gtoken Co-authored-by: Daniel Bankhead --- package.json | 2 +- src/auth/jwtclient.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a10c938..7a4a7037 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "fast-text-encoding": "^1.0.0", "gaxios": "^5.0.0", "gcp-metadata": "^5.0.0", - "gtoken": "^6.0.0", + "gtoken": "^6.1.0", "jws": "^4.0.0", "lru-cache": "^6.0.0" }, diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index acacd2b4..0d851303 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -192,6 +192,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { keyFile: this.keyFile, key: this.key, additionalClaims: {target_audience: targetAudience}, + transporter: this.transporter, }); await gtoken.getToken({ forceRefresh: true, @@ -285,6 +286,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { keyFile: this.keyFile, key: this.key, additionalClaims: this.additionalClaims, + transporter: this.transporter, }); } return this.gtoken; From 178e3b83104f5a050f09e17d522d36c8feca632c Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Tue, 23 Aug 2022 15:10:11 -0700 Subject: [PATCH 375/662] feat: adding configurable token lifespan support (#1441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adding support for configurable token lifespan * fix: fixing lint error and adding documentation * fix: changing readme * fix: removing unintentional whitespace * fix: minor readme edits * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Daniel Bankhead Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> Co-authored-by: Owl Bot --- .readme-partials.yaml | 28 +++++++++++++++ README.md | 28 +++++++++++++++ samples/test/externalclient.test.js | 38 +++++++++++++++++++- src/auth/baseexternalclient.ts | 10 ++++++ test/externalclienthelper.ts | 4 +++ test/test.baseexternalclient.ts | 54 +++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+), 1 deletion(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 5c995155..1ca51c7d 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -607,6 +607,34 @@ body: |- You can now [use the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC or SAML provider. + #### Configurable Token Lifetime + When creating a credential configuration with workload identity federation using service account impersonation, you can provide an optional argument to configure the service account access token lifetime. + + To generate the configuration with configurable token lifetime, run the following command (this example uses an AWS configuration, but the token lifetime can be configured for all workload identity federation providers): + + ```bash + # Generate an AWS configuration file with configurable token lifetime. + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AWS_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --aws \ + --output-file /path/to/generated/config.json \ + --service-account-token-lifetime-seconds $TOKEN_LIFETIME + ``` + + Where the following variables need to be substituted: + - `$PROJECT_NUMBER`: The Google Cloud project number. + - `$POOL_ID`: The workload identity pool ID. + - `$AWS_PROVIDER_ID`: The AWS provider ID. + - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + - `$TOKEN_LIFETIME`: The desired lifetime duration of the service account access token in seconds. + + The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. + The minimum allowed value is 600 (10 minutes) and the maximum allowed value is 43200 (12 hours). + If a lifetime greater than one hour is required, the service account must be added as an allowed value in an Organization Policy that enforces the `constraints/iam.allowServiceAccountCredentialLifetimeExtension` constraint. + + Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired. + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. diff --git a/README.md b/README.md index 059bd944..99dc4788 100644 --- a/README.md +++ b/README.md @@ -651,6 +651,34 @@ credentials unless they do not meet your specific requirements. You can now [use the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC or SAML provider. +#### Configurable Token Lifetime +When creating a credential configuration with workload identity federation using service account impersonation, you can provide an optional argument to configure the service account access token lifetime. + +To generate the configuration with configurable token lifetime, run the following command (this example uses an AWS configuration, but the token lifetime can be configured for all workload identity federation providers): + +```bash +# Generate an AWS configuration file with configurable token lifetime. +gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AWS_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --aws \ + --output-file /path/to/generated/config.json \ + --service-account-token-lifetime-seconds $TOKEN_LIFETIME +``` + + Where the following variables need to be substituted: +- `$PROJECT_NUMBER`: The Google Cloud project number. +- `$POOL_ID`: The workload identity pool ID. +- `$AWS_PROVIDER_ID`: The AWS provider ID. +- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. +- `$TOKEN_LIFETIME`: The desired lifetime duration of the service account access token in seconds. + +The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. +The minimum allowed value is 600 (10 minutes) and the maximum allowed value is 43200 (12 hours). +If a lifetime greater than one hour is required, the service account must be added as an allowed value in an Organization Policy that enforces the `constraints/iam.allowServiceAccountCredentialLifetimeExtension` constraint. + +Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired. + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js index a9f9023a..6efdc3f3 100644 --- a/samples/test/externalclient.test.js +++ b/samples/test/externalclient.test.js @@ -72,7 +72,11 @@ const {assert} = require('chai'); const {describe, it, before, afterEach} = require('mocha'); const fs = require('fs'); const {promisify} = require('util'); -const {GoogleAuth, DefaultTransporter} = require('google-auth-library'); +const { + GoogleAuth, + DefaultTransporter, + IdentityPoolClient, +} = require('google-auth-library'); const os = require('os'); const path = require('path'); const http = require('http'); @@ -472,4 +476,36 @@ describe('samples for external-account', () => { // Confirm expected script output. assert.match(output, /DNS Info:/); }); + + it('should acquire access token with service account impersonation options', async () => { + // Create file-sourced configuration JSON file. + // The created OIDC token will be used as the subject token and will be + // retrieved from a file location. + const config = { + type: 'external_account', + audience: AUDIENCE_OIDC, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: 'https://sts.googleapis.com/v1/token', + service_account_impersonation_url: + 'https://iamcredentials.googleapis.com/v1/projects/' + + `-/serviceAccounts/${clientEmail}:generateAccessToken`, + service_account_impersonation: { + token_lifetime_seconds: 2800, + }, + credential_source: { + file: oidcTokenFilePath, + }, + }; + await writeFile(oidcTokenFilePath, oidcToken); + const client = new IdentityPoolClient(config); + + const minExpireTime = new Date().getTime() + (2800 * 1000 - 5 * 1000); + const maxExpireTime = new Date().getTime() + (2800 * 1000 + 5 * 1000); + const token = await client.getAccessToken(); + const actualExpireTime = new Date(token.res.data.expireTime).getTime(); + + assert.isTrue( + minExpireTime <= actualExpireTime && actualExpireTime <= maxExpireTime + ); + }); }); diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 87442634..db108566 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -41,6 +41,8 @@ const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'; const GOOGLE_APIS_DOMAIN_PATTERN = '\\.googleapis\\.com$'; /** The variable portion pattern in a Google APIs domain. */ const VARIABLE_PORTION_PATTERN = '[^\\.\\s\\/\\\\]+'; +/** Default impersonated token lifespan in seconds.*/ +const DEFAULT_TOKEN_LIFESPAN = 3600; /** * Offset to take into account network delays and server clock skews. @@ -69,6 +71,9 @@ export interface BaseExternalAccountClientOptions { audience: string; subject_token_type: string; service_account_impersonation_url?: string; + service_account_impersonation?: { + token_lifetime_seconds?: number; + }; token_url: string; token_info_url?: string; client_id?: string; @@ -130,6 +135,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { protected readonly audience: string; protected readonly subjectTokenType: string; private readonly serviceAccountImpersonationUrl?: string; + private readonly serviceAccountImpersonationLifetime?: number; private readonly stsCredential: sts.StsCredentials; private readonly clientAuth?: ClientAuthentication; private readonly workforcePoolUserProject?: string; @@ -203,6 +209,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { } this.serviceAccountImpersonationUrl = options.service_account_impersonation_url; + this.serviceAccountImpersonationLifetime = + options.service_account_impersonation?.token_lifetime_seconds ?? + DEFAULT_TOKEN_LIFESPAN; // As threshold could be zero, // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the // zero value. @@ -510,6 +519,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { }, data: { scope: this.getScopesArray(), + lifetime: this.serviceAccountImpersonationLifetime + 's', }, responseType: 'json', }; diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 9ab9d4d1..0206aefa 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -44,8 +44,10 @@ interface NockMockGenerateAccessToken { token: string; response: IamGenerateAccessTokenResponse | CloudRequestError; scopes: string[]; + lifetime?: number; } +const defaultLifetime = 3600; const defaultProjectNumber = '123456'; const poolId = 'POOL_ID'; const providerId = 'PROVIDER_ID'; @@ -86,6 +88,8 @@ export function mockGenerateAccessToken( saPath, { scope: nockMockGenerateAccessToken.scopes, + lifetime: + (nockMockGenerateAccessToken.lifetime ?? defaultLifetime) + 's', }, { reqheaders: { diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 72acd595..5d323003 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -1623,6 +1623,60 @@ describe('BaseExternalAccountClient', () => { }); scopes.forEach(scope => scope.done()); }); + + it('should use provided token lifespan', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + scopes.push( + mockGenerateAccessToken([ + { + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + lifetime: 2800, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + ]) + ); + + const externalAccountOptionsWithSATokenLifespan = Object.assign( + { + service_account_impersonation: { + token_lifetime_seconds: 2800, + }, + }, + externalAccountOptionsWithSA + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSATokenLifespan + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); }); }); From b484475a60c7272cd958e305077e077ffa5cc2df Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 23 Aug 2022 15:43:41 -0700 Subject: [PATCH 376/662] chore(main): release 8.4.0 (#1442) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c2af10..f1a4ff99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.4.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.3.0...v8.4.0) (2022-08-23) + + +### Features + +* adding configurable token lifespan support ([#1441](https://github.com/googleapis/google-auth-library-nodejs/issues/1441)) ([178e3b8](https://github.com/googleapis/google-auth-library-nodejs/commit/178e3b83104f5a050f09e17d522d36c8feca632c)) + + +### Bug Fixes + +* **functions:** clarify auth comments ([#1427](https://github.com/googleapis/google-auth-library-nodejs/issues/1427)) ([7e06732](https://github.com/googleapis/google-auth-library-nodejs/commit/7e067327634281bba948c9cc6bc99c7ab860f827)) + ## [8.3.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.2.0...v8.3.0) (2022-08-19) diff --git a/package.json b/package.json index 7a4a7037..3c2b8f6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.3.0", + "version": "8.4.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 060ce9a2..0621b454 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.3.0", + "google-auth-library": "^8.4.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 54afa8ef184c8a68c9930d67d850b7334c28ecaf Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 25 Aug 2022 21:50:26 -0700 Subject: [PATCH 377/662] fix: add hashes to requirements.txt (#1544) (#1449) * fix: add hashes to requirements.txt and update Docker images so they require hashes. * fix: add hashes to docker/owlbot/java/src * Squashed commit of the following: commit ab7384ea1c30df8ec2e175566ef2508e6c3a2acb Author: Jeffrey Rennie Date: Tue Aug 23 11:38:48 2022 -0700 fix: remove pip install statements (#1546) because the tools are already installed in the docker image as of https://github.com/googleapis/testing-infra-docker/pull/227 commit 302667c9ab7210da42cc337e8f39fe1ea99049ef Author: WhiteSource Renovate Date: Tue Aug 23 19:50:28 2022 +0200 chore(deps): update dependency setuptools to v65.2.0 (#1541) Co-authored-by: Anthonios Partheniou commit 6e9054fd91d1b500cae58ff72ee9aeb626077756 Author: WhiteSource Renovate Date: Tue Aug 23 19:42:51 2022 +0200 chore(deps): update dependency nbconvert to v7 (#1543) Co-authored-by: Anthonios Partheniou commit d229a1258999f599a90a9b674a1c5541e00db588 Author: Alexander Fenster Date: Mon Aug 22 15:04:53 2022 -0700 fix: update google-gax and remove obsolete deps (#1545) commit 13ce62621e70059b2f5e3a7bade735f91c53339c Author: Jeffrey Rennie Date: Mon Aug 22 11:08:21 2022 -0700 chore: remove release config and script (#1540) We don't release to pypi anymore. * chore: rollback java changes to move forward with other languages until Java's docker image is fixed Source-Link: https://github.com/googleapis/synthtool/commit/48263378ad6010ec2fc4d480af7b5d08170338c8 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:7fefeb9e517db2dd8c8202d9239ff6788d6852bc92dd3aac57a46059679ac9de Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/publish.sh | 1 - .kokoro/release/docs.sh | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index f3ca5561..cb86baf8 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:ddb19a6df6c1fa081bc99fb29658f306dd64668bc26f75d1353b28296f3a78e6 -# created: 2022-06-07T21:18:30.024751809Z + digest: sha256:7fefeb9e517db2dd8c8202d9239ff6788d6852bc92dd3aac57a46059679ac9de +# created: 2022-08-24T19:44:03.464675104Z diff --git a/.kokoro/publish.sh b/.kokoro/publish.sh index 77a5defb..949e3e1d 100755 --- a/.kokoro/publish.sh +++ b/.kokoro/publish.sh @@ -19,7 +19,6 @@ set -eo pipefail export NPM_CONFIG_PREFIX=${HOME}/.npm-global # Start the releasetool reporter -python3 -m pip install gcp-releasetool python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script cd $(dirname $0)/.. diff --git a/.kokoro/release/docs.sh b/.kokoro/release/docs.sh index 4c866c86..1d8f3f49 100755 --- a/.kokoro/release/docs.sh +++ b/.kokoro/release/docs.sh @@ -29,7 +29,6 @@ npm run docs # create docs.metadata, based on package.json and .repo-metadata.json. npm i json@9.0.6 -g -python3 -m pip install --user gcp-docuploader python3 -m docuploader create-metadata \ --name=$(cat .repo-metadata.json | json name) \ --version=$(cat package.json | json version) \ From 8d7e11319143a121cb0a999637b8e5988598ad4c Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:20:40 -0700 Subject: [PATCH 378/662] docs: adding workforce documentation to readme (#1450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adding workforce documentation to readme * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot --- .readme-partials.yaml | 197 ++++++++++++++++++++++++++++++++++++++++++ README.md | 197 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 394 insertions(+) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 1ca51c7d..f73558b1 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -8,6 +8,7 @@ body: |- - [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication. - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). + - [Workforce Identity Federation](#workforce-identity-federation) - Use workforce identity federation to access Google Cloud resources using an external identity provider (IdP) to authenticate and authorize a workforce—a group of users, such as employees, partners, and contractors—using IAM, so that the users can access Google Cloud services. - [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account. - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. @@ -635,6 +636,202 @@ body: |- Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired. + ## Workforce Identity Federation + + [Workforce identity federation](https://cloud.google.com/iam/docs/workforce-identity-federation) lets you use an + external identity provider (IdP) to authenticate and authorize a workforce—a group of users, such as employees, + partners, and contractors—using IAM, so that the users can access Google Cloud services. Workforce identity federation + extends Google Cloud's identity capabilities to support syncless, attribute-based single sign on. + + With workforce identity federation, your workforce can access Google Cloud resources using an external + identity provider (IdP) that supports OpenID Connect (OIDC) or SAML 2.0 such as Azure Active Directory (Azure AD), + Active Directory Federation Services (AD FS), Okta, and others. + + ### Accessing resources using an OIDC or SAML 2.0 identity provider + + In order to access Google Cloud resources from an identity provider that supports [OpenID Connect (OIDC)](https://openid.net/connect/), + the following requirements are needed: + - A workforce identity pool needs to be created. + - An OIDC or SAML 2.0 identity provider needs to be added in the workforce pool. + + Follow the detailed [instructions](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation) on how + to configure workforce identity federation. + + After configuring an OIDC or SAML 2.0 provider, a credential configuration + file needs to be generated. The generated credential configuration file contains non-sensitive metadata to instruct the + library on how to retrieve external subject tokens and exchange them for GCP access tokens. + The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). + + The Auth library can retrieve external subject tokens from a local file location + (file-sourced credentials), from a local server (URL-sourced credentials) or by calling an executable + (executable-sourced credentials). + + **File-sourced credentials** + For file-sourced credentials, a background process needs to be continuously refreshing the file + location with a new subject token prior to expiration. For tokens with one hour lifetimes, the token + needs to be updated in the file every hour. The token can be stored directly as plain text or in + JSON format. + + To generate a file-sourced OIDC configuration, run the following command: + + ```bash + # Generate an OIDC configuration file for file-sourced credentials. + gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --subject-token-type=urn:ietf:params:oauth:token-type:id_token \ + --credential-source-file=$PATH_TO_OIDC_ID_TOKEN \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + # Optional arguments for file types. Default is "text": + # --credential-source-type "json" \ + # Optional argument for the field that contains the OIDC credential. + # This is required for json. + # --credential-source-field-name "id_token" \ + --output-file=/path/to/generated/config.json + ``` + Where the following variables need to be substituted: + - `$WORKFORCE_POOL_ID`: The workforce pool ID. + - `$PROVIDER_ID`: The provider ID. + - `$PATH_TO_OIDC_ID_TOKEN`: The file path used to retrieve the OIDC token. + - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + + To generate a file-sourced SAML configuration, run the following command: + + ```bash + # Generate a SAML configuration file for file-sourced credentials. + gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --credential-source-file=$PATH_TO_SAML_ASSERTION \ + --subject-token-type=urn:ietf:params:oauth:token-type:saml2 \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + --output-file=/path/to/generated/config.json + ``` + + Where the following variables need to be substituted: + - `$WORKFORCE_POOL_ID`: The workforce pool ID. + - `$PROVIDER_ID`: The provider ID. + - `$PATH_TO_SAML_ASSERTION`: The file path used to retrieve the base64-encoded SAML assertion. + - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + + These commands generate the configuration file in the specified output file. + + **URL-sourced credentials** + For URL-sourced credentials, a local server needs to host a GET endpoint to return the OIDC token. + The response can be in plain text or JSON. Additional required request headers can also be + specified. + + To generate a URL-sourced OIDC workforce identity configuration, run the following command: + + ```bash + # Generate an OIDC configuration file for URL-sourced credentials. + gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --subject-token-type=urn:ietf:params:oauth:token-type:id_token \ + --credential-source-url=$URL_TO_RETURN_OIDC_ID_TOKEN \ + --credential-source-headers $HEADER_KEY=$HEADER_VALUE \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + --output-file=/path/to/generated/config.json + ``` + + Where the following variables need to be substituted: + - `$WORKFORCE_POOL_ID`: The workforce pool ID. + - `$PROVIDER_ID`: The provider ID. + - `$URL_TO_RETURN_OIDC_ID_TOKEN`: The URL of the local server endpoint. + - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to + `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. + - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + + To generate a URL-sourced SAML configuration, run the following command: + + ```bash + # Generate a SAML configuration file for file-sourced credentials. + gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --subject-token-type=urn:ietf:params:oauth:token-type:saml2 \ + --credential-source-url=$URL_TO_GET_SAML_ASSERTION \ + --credential-source-headers $HEADER_KEY=$HEADER_VALUE \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + --output-file=/path/to/generated/config.json + ``` + + These commands generate the configuration file in the specified output file. + + Where the following variables need to be substituted: + - `$WORKFORCE_POOL_ID`: The workforce pool ID. + - `$PROVIDER_ID`: The provider ID. + - `$URL_TO_GET_SAML_ASSERTION`: The URL of the local server endpoint. + - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to + `$URL_TO_GET_SAML_ASSERTION`, e.g. `Metadata-Flavor=Google`. + - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + + ### Using Executable-sourced workforce credentials with OIDC and SAML + + **Executable-sourced credentials** + For executable-sourced credentials, a local executable is used to retrieve the 3rd party token. + The executable must handle providing a valid, unexpired OIDC ID token or SAML assertion in JSON format + to stdout. + + To use executable-sourced credentials, the `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES` + environment variable must be set to `1`. + + To generate an executable-sourced workforce identity configuration, run the following command: + + ```bash + # Generate a configuration file for executable-sourced credentials. + gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --subject-token-type=$SUBJECT_TOKEN_TYPE \ + # The absolute path for the program, including arguments. + # e.g. --executable-command="/path/to/command --foo=bar" + --executable-command=$EXECUTABLE_COMMAND \ + # Optional argument for the executable timeout. Defaults to 30s. + # --executable-timeout-millis=$EXECUTABLE_TIMEOUT \ + # Optional argument for the absolute path to the executable output file. + # See below on how this argument impacts the library behaviour. + # --executable-output-file=$EXECUTABLE_OUTPUT_FILE \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + --output-file /path/to/generated/config.json + ``` + Where the following variables need to be substituted: + - `$WORKFORCE_POOL_ID`: The workforce pool ID. + - `$PROVIDER_ID`: The provider ID. + - `$SUBJECT_TOKEN_TYPE`: The subject token type. + - `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program. + - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + + The `--executable-timeout-millis` flag is optional. This is the duration for which + the auth library will wait for the executable to finish, in milliseconds. + Defaults to 30 seconds when not provided. The maximum allowed value is 2 minutes. + The minimum is 5 seconds. + + The `--executable-output-file` flag is optional. If provided, the file path must + point to the 3rd party credential response generated by the executable. This is useful + for caching the credentials. By specifying this path, the Auth libraries will first + check for its existence before running the executable. By caching the executable JSON + response to this file, it improves performance as it avoids the need to run the executable + until the cached credentials in the output file are expired. The executable must + handle writing to this file - the auth libraries will only attempt to read from + this location. The format of contents in the file should match the JSON format + expected by the executable shown below. + + To retrieve the 3rd party token, the library will call the executable + using the command specified. The executable's output must adhere to the response format + specified below. It must output the response to stdout. + + Refer to the [using executable-sourced credentials with Workload Identity Federation](#using-executable-sourced-credentials-with-oidc-and-saml) + above for the executable response specification. + + ##### Security considerations + The following security practices are highly recommended: + * Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script. + * The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion. + + Given the complexity of using executable-sourced credentials, it is recommended to use + the existing supported mechanisms (file-sourced/URL-sourced) for providing 3rd party + credentials unless they do not meet your specific requirements. + + You can now [use the Auth library](#using-external-identities) to call Google Cloud + resources from an OIDC or SAML provider. + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. diff --git a/README.md b/README.md index 99dc4788..6cc36cd9 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ This library provides a variety of ways to authenticate to your Google services. - [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication. - [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication. - [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). +- [Workforce Identity Federation](#workforce-identity-federation) - Use workforce identity federation to access Google Cloud resources using an external identity provider (IdP) to authenticate and authorize a workforce—a group of users, such as employees, partners, and contractors—using IAM, so that the users can access Google Cloud services. - [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account. - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. @@ -679,6 +680,202 @@ If a lifetime greater than one hour is required, the service account must be add Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired. +## Workforce Identity Federation + +[Workforce identity federation](https://cloud.google.com/iam/docs/workforce-identity-federation) lets you use an +external identity provider (IdP) to authenticate and authorize a workforce—a group of users, such as employees, +partners, and contractors—using IAM, so that the users can access Google Cloud services. Workforce identity federation +extends Google Cloud's identity capabilities to support syncless, attribute-based single sign on. + +With workforce identity federation, your workforce can access Google Cloud resources using an external +identity provider (IdP) that supports OpenID Connect (OIDC) or SAML 2.0 such as Azure Active Directory (Azure AD), +Active Directory Federation Services (AD FS), Okta, and others. + +### Accessing resources using an OIDC or SAML 2.0 identity provider + +In order to access Google Cloud resources from an identity provider that supports [OpenID Connect (OIDC)](https://openid.net/connect/), +the following requirements are needed: +- A workforce identity pool needs to be created. +- An OIDC or SAML 2.0 identity provider needs to be added in the workforce pool. + +Follow the detailed [instructions](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation) on how +to configure workforce identity federation. + +After configuring an OIDC or SAML 2.0 provider, a credential configuration +file needs to be generated. The generated credential configuration file contains non-sensitive metadata to instruct the +library on how to retrieve external subject tokens and exchange them for GCP access tokens. +The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). + +The Auth library can retrieve external subject tokens from a local file location +(file-sourced credentials), from a local server (URL-sourced credentials) or by calling an executable +(executable-sourced credentials). + +**File-sourced credentials** +For file-sourced credentials, a background process needs to be continuously refreshing the file +location with a new subject token prior to expiration. For tokens with one hour lifetimes, the token +needs to be updated in the file every hour. The token can be stored directly as plain text or in +JSON format. + +To generate a file-sourced OIDC configuration, run the following command: + +```bash +# Generate an OIDC configuration file for file-sourced credentials. +gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --subject-token-type=urn:ietf:params:oauth:token-type:id_token \ + --credential-source-file=$PATH_TO_OIDC_ID_TOKEN \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + # Optional arguments for file types. Default is "text": + # --credential-source-type "json" \ + # Optional argument for the field that contains the OIDC credential. + # This is required for json. + # --credential-source-field-name "id_token" \ + --output-file=/path/to/generated/config.json +``` +Where the following variables need to be substituted: +- `$WORKFORCE_POOL_ID`: The workforce pool ID. +- `$PROVIDER_ID`: The provider ID. +- `$PATH_TO_OIDC_ID_TOKEN`: The file path used to retrieve the OIDC token. +- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + +To generate a file-sourced SAML configuration, run the following command: + +```bash +# Generate a SAML configuration file for file-sourced credentials. +gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --credential-source-file=$PATH_TO_SAML_ASSERTION \ + --subject-token-type=urn:ietf:params:oauth:token-type:saml2 \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + --output-file=/path/to/generated/config.json +``` + +Where the following variables need to be substituted: +- `$WORKFORCE_POOL_ID`: The workforce pool ID. +- `$PROVIDER_ID`: The provider ID. +- `$PATH_TO_SAML_ASSERTION`: The file path used to retrieve the base64-encoded SAML assertion. +- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + +These commands generate the configuration file in the specified output file. + +**URL-sourced credentials** +For URL-sourced credentials, a local server needs to host a GET endpoint to return the OIDC token. +The response can be in plain text or JSON. Additional required request headers can also be +specified. + +To generate a URL-sourced OIDC workforce identity configuration, run the following command: + +```bash +# Generate an OIDC configuration file for URL-sourced credentials. +gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --subject-token-type=urn:ietf:params:oauth:token-type:id_token \ + --credential-source-url=$URL_TO_RETURN_OIDC_ID_TOKEN \ + --credential-source-headers $HEADER_KEY=$HEADER_VALUE \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + --output-file=/path/to/generated/config.json +``` + +Where the following variables need to be substituted: +- `$WORKFORCE_POOL_ID`: The workforce pool ID. +- `$PROVIDER_ID`: The provider ID. +- `$URL_TO_RETURN_OIDC_ID_TOKEN`: The URL of the local server endpoint. +- `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to + `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. +- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + +To generate a URL-sourced SAML configuration, run the following command: + +```bash +# Generate a SAML configuration file for file-sourced credentials. +gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --subject-token-type=urn:ietf:params:oauth:token-type:saml2 \ + --credential-source-url=$URL_TO_GET_SAML_ASSERTION \ + --credential-source-headers $HEADER_KEY=$HEADER_VALUE \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + --output-file=/path/to/generated/config.json +``` + +These commands generate the configuration file in the specified output file. + +Where the following variables need to be substituted: +- `$WORKFORCE_POOL_ID`: The workforce pool ID. +- `$PROVIDER_ID`: The provider ID. +- `$URL_TO_GET_SAML_ASSERTION`: The URL of the local server endpoint. +- `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to + `$URL_TO_GET_SAML_ASSERTION`, e.g. `Metadata-Flavor=Google`. +- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + +### Using Executable-sourced workforce credentials with OIDC and SAML + +**Executable-sourced credentials** +For executable-sourced credentials, a local executable is used to retrieve the 3rd party token. +The executable must handle providing a valid, unexpired OIDC ID token or SAML assertion in JSON format +to stdout. + +To use executable-sourced credentials, the `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES` +environment variable must be set to `1`. + +To generate an executable-sourced workforce identity configuration, run the following command: + +```bash +# Generate a configuration file for executable-sourced credentials. +gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \ + --subject-token-type=$SUBJECT_TOKEN_TYPE \ + # The absolute path for the program, including arguments. + # e.g. --executable-command="/path/to/command --foo=bar" + --executable-command=$EXECUTABLE_COMMAND \ + # Optional argument for the executable timeout. Defaults to 30s. + # --executable-timeout-millis=$EXECUTABLE_TIMEOUT \ + # Optional argument for the absolute path to the executable output file. + # See below on how this argument impacts the library behaviour. + # --executable-output-file=$EXECUTABLE_OUTPUT_FILE \ + --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ + --output-file /path/to/generated/config.json +``` +Where the following variables need to be substituted: +- `$WORKFORCE_POOL_ID`: The workforce pool ID. +- `$PROVIDER_ID`: The provider ID. +- `$SUBJECT_TOKEN_TYPE`: The subject token type. +- `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program. +- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + +The `--executable-timeout-millis` flag is optional. This is the duration for which +the auth library will wait for the executable to finish, in milliseconds. +Defaults to 30 seconds when not provided. The maximum allowed value is 2 minutes. +The minimum is 5 seconds. + +The `--executable-output-file` flag is optional. If provided, the file path must +point to the 3rd party credential response generated by the executable. This is useful +for caching the credentials. By specifying this path, the Auth libraries will first +check for its existence before running the executable. By caching the executable JSON +response to this file, it improves performance as it avoids the need to run the executable +until the cached credentials in the output file are expired. The executable must +handle writing to this file - the auth libraries will only attempt to read from +this location. The format of contents in the file should match the JSON format +expected by the executable shown below. + +To retrieve the 3rd party token, the library will call the executable +using the command specified. The executable's output must adhere to the response format +specified below. It must output the response to stdout. + +Refer to the [using executable-sourced credentials with Workload Identity Federation](#using-executable-sourced-credentials-with-oidc-and-saml) +above for the executable response specification. + +##### Security considerations +The following security practices are highly recommended: +* Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script. +* The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion. + +Given the complexity of using executable-sourced credentials, it is recommended to use +the existing supported mechanisms (file-sourced/URL-sourced) for providing 3rd party +credentials unless they do not meet your specific requirements. + +You can now [use the Auth library](#using-external-identities) to call Google Cloud +resources from an OIDC or SAML provider. + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. From 6c04661c6950532a8239cb95f0509a9d5368ffcc Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 30 Aug 2022 08:52:10 -0700 Subject: [PATCH 379/662] fix: remove `projectId` check for `signBlob` calls --- src/auth/googleauth.ts | 67 ++++++++++++++++++++++------------------- test/test.googleauth.ts | 15 ++++++--- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index fa8d0cb2..f7718754 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -134,7 +134,7 @@ export class GoogleAuth { return this.checkIsGCE; } - private _getDefaultProjectIdPromise?: Promise; + private _findProjectIdPromise?: Promise; private _cachedProjectId?: string | null; // To save the contents of the JSON credential file @@ -191,37 +191,47 @@ export class GoogleAuth { } } + /** + * A private method for finding and caching a projectId. + * + * Supports environments in order of precedence: + * - 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 + * + * @returns projectId + */ + async #findAndCacheProjectId(): Promise { + let projectId: string | null | undefined = null; + + projectId ||= await this.getProductionProjectId(); + projectId ||= await this.getFileProjectId(); + projectId ||= await this.getDefaultServiceProjectId(); + projectId ||= await this.getGCEProjectId(); + projectId ||= await this.getExternalAccountClientProjectId(); + + if (projectId) { + this._cachedProjectId = projectId; + return projectId; + } else { + 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' + ); + } + } + private getProjectIdAsync(): Promise { 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) { - this._getDefaultProjectIdPromise = (async () => { - 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' - ); - } - return projectId; - })(); + if (!this._findProjectIdPromise) { + this._findProjectIdPromise = this.#findAndCacheProjectId(); } - return this._getDefaultProjectIdPromise; + return this._findProjectIdPromise; } /** @@ -951,11 +961,6 @@ export class GoogleAuth { return sign; } - 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`.'); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 2bf13828..2f3f990a 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1459,9 +1459,16 @@ describe('googleauth', () => { assert.strictEqual(value, computed); }); - it('sign should hit the IAM endpoint if no private_key is available', async () => { + it('sign should hit the IAM endpoint if no projectId nor private_key is available', async () => { const {auth, scopes} = mockGCE(); - mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + + sinon + .stub( + auth as unknown as {getProjectIdAsync: () => Promise}, + 'getProjectIdAsync' + ) + .resolves(); + const email = 'google@auth.library'; const iamUri = 'https://iamcredentials.googleapis.com'; const iamPath = `/v1/projects/-/serviceAccounts/${email}:signBlob`; @@ -2264,7 +2271,6 @@ describe('googleauth', () => { describe('sign()', () => { it('should reject when no impersonation is used', async () => { - const scopes = mockGetAccessTokenAndProjectId(); const auth = new GoogleAuth({ credentials: createExternalAccountJSON(), }); @@ -2273,12 +2279,11 @@ describe('googleauth', () => { auth.sign('abc123'), /Cannot sign data without `client_email`/ ); - scopes.forEach(s => s.done()); }); it('should use IAMCredentials endpoint when impersonation is used', async () => { const scopes = mockGetAccessTokenAndProjectId( - true, + false, ['https://www.googleapis.com/auth/cloud-platform'], true ); From b37489b6bc17645d3ea23fbceb2326adb296240b Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 31 Aug 2022 09:54:12 -0700 Subject: [PATCH 380/662] feat: Support Not Requiring `projectId` When Not Required (#1448) * feat: Support Not Requiring `projectId` When Not Required * test: Add test for `null` projectId support --- src/auth/googleauth.ts | 49 ++++++++++++++++++++++++++++++++--------- test/test.googleauth.ts | 23 +++++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index f7718754..415e5875 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -116,6 +116,13 @@ export interface GoogleAuthOptions { export const CLOUD_SDK_CLIENT_ID = '764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com'; +const GoogleAuthExceptionMessages = { + NO_PROJECT_ID_FOUND: + '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', +} as const; + export class GoogleAuth { transporter?: Transporter; @@ -192,6 +199,29 @@ export class GoogleAuth { } /** + * A temporary method for internal `getProjectId` usages where `null` is + * acceptable. In a future major release, `getProjectId` should return `null` + * (as the `Promise` base signature describes) and this private + * method should be removed. + * + * @returns Promise that resolves with project id (or `null`) + */ + async #getProjectIdOptional(): Promise { + try { + return await this.getProjectId(); + } catch (e) { + if ( + e instanceof Error && + e.message === GoogleAuthExceptionMessages.NO_PROJECT_ID_FOUND + ) { + return null; + } else { + throw e; + } + } + } + + /* * A private method for finding and caching a projectId. * * Supports environments in order of precedence: @@ -215,17 +245,13 @@ export class GoogleAuth { this._cachedProjectId = projectId; return projectId; } else { - 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' - ); + throw new Error(GoogleAuthExceptionMessages.NO_PROJECT_ID_FOUND); } } - private getProjectIdAsync(): Promise { + private async getProjectIdAsync(): Promise { if (this._cachedProjectId) { - return Promise.resolve(this._cachedProjectId); + return this._cachedProjectId; } if (!this._findProjectIdPromise) { @@ -279,7 +305,7 @@ export class GoogleAuth { if (this.cachedCredential) { return { credential: this.cachedCredential, - projectId: await this.getProjectIdAsync(), + projectId: await this.#getProjectIdOptional(), }; } @@ -297,7 +323,8 @@ export class GoogleAuth { credential.scopes = this.getAnyScopes(); } this.cachedCredential = credential; - projectId = await this.getProjectId(); + projectId = await this.#getProjectIdOptional(); + return {credential, projectId}; } @@ -312,7 +339,7 @@ export class GoogleAuth { credential.scopes = this.getAnyScopes(); } this.cachedCredential = credential; - projectId = await this.getProjectId(); + projectId = await this.#getProjectIdOptional(); return {credential, projectId}; } @@ -339,7 +366,7 @@ export class GoogleAuth { // the rest. (options as ComputeOptions).scopes = this.getAnyScopes(); this.cachedCredential = new Compute(options); - projectId = await this.getProjectId(); + projectId = await this.#getProjectIdOptional(); return {projectId, credential: this.cachedCredential}; } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 2f3f990a..ca584254 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1998,6 +1998,29 @@ describe('googleauth', () => { ); scopes.forEach(s => s.done()); }); + + it('should return `null` for `projectId` when on cannot be found', async () => { + // Environment variable is set up to point to external-account-cred.json + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-cred.json' + ); + + const auth = new GoogleAuth(); + + sandbox + .stub( + auth as {} as { + getProjectIdAsync: Promise; + }, + 'getProjectIdAsync' + ) + .resolves(null); + + const res = await auth.getApplicationDefault(); + + assert.equal(res.projectId, null); + }); }); describe('getApplicationCredentialsFromFilePath()', () => { From bd30098ea2d07f16c84367590d51beefa08e34e5 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 31 Aug 2022 17:04:12 +0000 Subject: [PATCH 381/662] chore(main): release 8.5.0 (#1451) :robot: I have created a release *beep* *boop* --- ## [8.5.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.4.0...v8.5.0) (2022-08-31) ### Features * Support Not Requiring `projectId` When Not Required ([#1448](https://github.com/googleapis/google-auth-library-nodejs/issues/1448)) ([b37489b](https://github.com/googleapis/google-auth-library-nodejs/commit/b37489b6bc17645d3ea23fbceb2326adb296240b)) ### Bug Fixes * add hashes to requirements.txt ([#1544](https://github.com/googleapis/google-auth-library-nodejs/issues/1544)) ([#1449](https://github.com/googleapis/google-auth-library-nodejs/issues/1449)) ([54afa8e](https://github.com/googleapis/google-auth-library-nodejs/commit/54afa8ef184c8a68c9930d67d850b7334c28ecaf)) * remove `projectId` check for `signBlob` calls ([6c04661](https://github.com/googleapis/google-auth-library-nodejs/commit/6c04661c6950532a8239cb95f0509a9d5368ffcc)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a4ff99..08ff99ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.5.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.4.0...v8.5.0) (2022-08-31) + + +### Features + +* Support Not Requiring `projectId` When Not Required ([#1448](https://github.com/googleapis/google-auth-library-nodejs/issues/1448)) ([b37489b](https://github.com/googleapis/google-auth-library-nodejs/commit/b37489b6bc17645d3ea23fbceb2326adb296240b)) + + +### Bug Fixes + +* add hashes to requirements.txt ([#1544](https://github.com/googleapis/google-auth-library-nodejs/issues/1544)) ([#1449](https://github.com/googleapis/google-auth-library-nodejs/issues/1449)) ([54afa8e](https://github.com/googleapis/google-auth-library-nodejs/commit/54afa8ef184c8a68c9930d67d850b7334c28ecaf)) +* remove `projectId` check for `signBlob` calls ([6c04661](https://github.com/googleapis/google-auth-library-nodejs/commit/6c04661c6950532a8239cb95f0509a9d5368ffcc)) + ## [8.4.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.3.0...v8.4.0) (2022-08-23) diff --git a/package.json b/package.json index 3c2b8f6c..99e33684 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.4.0", + "version": "8.5.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 0621b454..bb267874 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.4.0", + "google-auth-library": "^8.5.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 6c302746c9eb4778619dec5f2f5f6e940af8086a Mon Sep 17 00:00:00 2001 From: Alexander Fenster Date: Wed, 31 Aug 2022 16:14:42 -0700 Subject: [PATCH 382/662] fix: do not use #private (#1454) --- src/auth/googleauth.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 415e5875..f288565f 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -206,7 +206,7 @@ export class GoogleAuth { * * @returns Promise that resolves with project id (or `null`) */ - async #getProjectIdOptional(): Promise { + private async getProjectIdOptional(): Promise { try { return await this.getProjectId(); } catch (e) { @@ -232,7 +232,7 @@ export class GoogleAuth { * * @returns projectId */ - async #findAndCacheProjectId(): Promise { + private async findAndCacheProjectId(): Promise { let projectId: string | null | undefined = null; projectId ||= await this.getProductionProjectId(); @@ -255,7 +255,7 @@ export class GoogleAuth { } if (!this._findProjectIdPromise) { - this._findProjectIdPromise = this.#findAndCacheProjectId(); + this._findProjectIdPromise = this.findAndCacheProjectId(); } return this._findProjectIdPromise; } @@ -305,7 +305,7 @@ export class GoogleAuth { if (this.cachedCredential) { return { credential: this.cachedCredential, - projectId: await this.#getProjectIdOptional(), + projectId: await this.getProjectIdOptional(), }; } @@ -323,7 +323,7 @@ export class GoogleAuth { credential.scopes = this.getAnyScopes(); } this.cachedCredential = credential; - projectId = await this.#getProjectIdOptional(); + projectId = await this.getProjectIdOptional(); return {credential, projectId}; } @@ -339,7 +339,7 @@ export class GoogleAuth { credential.scopes = this.getAnyScopes(); } this.cachedCredential = credential; - projectId = await this.#getProjectIdOptional(); + projectId = await this.getProjectIdOptional(); return {credential, projectId}; } @@ -366,7 +366,7 @@ export class GoogleAuth { // the rest. (options as ComputeOptions).scopes = this.getAnyScopes(); this.cachedCredential = new Compute(options); - projectId = await this.#getProjectIdOptional(); + projectId = await this.getProjectIdOptional(); return {projectId, credential: this.cachedCredential}; } From 841f1f2b4f736b727593f8c472467515d84a7c58 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 31 Aug 2022 23:20:14 +0000 Subject: [PATCH 383/662] chore(main): release 8.5.1 (#1455) :robot: I have created a release *beep* *boop* --- ## [8.5.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.5.0...v8.5.1) (2022-08-31) ### Bug Fixes * do not use #private ([#1454](https://github.com/googleapis/google-auth-library-nodejs/issues/1454)) ([6c30274](https://github.com/googleapis/google-auth-library-nodejs/commit/6c302746c9eb4778619dec5f2f5f6e940af8086a)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08ff99ac..90a0047b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +## [8.5.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.5.0...v8.5.1) (2022-08-31) + + +### Bug Fixes + +* do not use #private ([#1454](https://github.com/googleapis/google-auth-library-nodejs/issues/1454)) ([6c30274](https://github.com/googleapis/google-auth-library-nodejs/commit/6c302746c9eb4778619dec5f2f5f6e940af8086a)) + ## [8.5.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.4.0...v8.5.0) (2022-08-31) diff --git a/package.json b/package.json index 99e33684..e8f2bfa4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.5.0", + "version": "8.5.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index bb267874..bd702a50 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.5.0", + "google-auth-library": "^8.5.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From b317b5963c18a598fceb85d4f32cc8cd64bb9b7b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 15 Sep 2022 20:38:29 +0200 Subject: [PATCH 384/662] fix(deps): update dependency puppeteer to v17 (#1452) Co-authored-by: Daniel Bankhead --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e8f2bfa4..8352a339 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^16.0.0", + "puppeteer": "^17.0.0", "sinon": "^14.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index f79ca832..a094933e 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^8.0.0", - "puppeteer": "^16.0.0" + "puppeteer": "^17.0.0" } } From f921fc728083e8c954e58131fa75c07fef966164 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 16 Sep 2022 07:18:48 -0700 Subject: [PATCH 385/662] refactor: Use Payload as Message for ReAuth-related `invalid_grant` Errors (#1457) * refactor: Use payload as message for reauth-related `invalid_grant` * test: fix * refactor: use `Array.isArray` --- src/auth/oauth2client.ts | 31 +++++++++++++++++++++++-------- test/test.oauth2.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index de70e452..45bfe80f 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -540,7 +540,7 @@ export class OAuth2Client extends AuthClient { opts.client_id = opts.client_id || this._clientId; opts.redirect_uri = opts.redirect_uri || this.redirectUri; // Allow scopes to be passed either as array or a string - if (opts.scope instanceof Array) { + if (Array.isArray(opts.scope)) { opts.scope = opts.scope.join(' '); } const rootUrl = OAuth2Client.GOOGLE_OAUTH2_AUTH_BASE_URL_; @@ -688,13 +688,28 @@ export class OAuth2Client extends AuthClient { grant_type: 'refresh_token', }; - // request for new token - const res = await this.transporter.request({ - method: 'POST', - url, - data: querystring.stringify(data), - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - }); + let res: GaxiosResponse; + + try { + // request for new token + res = await this.transporter.request({ + method: 'POST', + url, + data: querystring.stringify(data), + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + }); + } catch (e) { + if ( + e instanceof GaxiosError && + e.message === 'invalid_grant' && + e.response?.data && + /ReAuth/i.test(e.response.data.error_description) + ) { + e.message = JSON.stringify(e.response.data); + } + + throw e; + } const tokens = res.data as Credentials; // TODO: de-duplicate this code from a few spots diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 763eb329..8152d011 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1007,6 +1007,37 @@ describe('oauth2', () => { assert.strictEqual('abc123', client.credentials.access_token); }); + it('should have a custom ReAuth error message', async () => { + // We have custom handling for make it easier for customers to resolve ReAuth errors + const reAuthErrorBody = { + error: 'invalid_grant', + error_description: 'a ReAuth error', + custom: 'property', + }; + + const scopes = [ + nock(baseUrl) + .post('/token', undefined, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .reply(500, reAuthErrorBody), + ]; + client.credentials = {refresh_token: 'refresh-token-placeholder'}; + + try { + await client.request({url: 'http://example.com'}); + } catch (e) { + assert(e instanceof GaxiosError); + assert(e.message.includes(JSON.stringify(reAuthErrorBody))); + + return; + } finally { + scopes.forEach(s => s.done()); + } + + throw new Error("expected an error, but didn't get one"); + }); + it('should refresh if access token is expired', done => { client.setCredentials({ access_token: 'initial-access-token', From a069469b9907df70967e2a1d91d2736fefdffe95 Mon Sep 17 00:00:00 2001 From: Alexander Fenster Date: Fri, 16 Sep 2022 10:20:20 -0700 Subject: [PATCH 386/662] docs: fix changelog link (#1461) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a0047b..472f4594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ [npm history][1] -[1]: https://www.npmjs.com/package/google-auth-library-nodejs?activeTab=versions +[1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions ## [8.5.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.5.0...v8.5.1) (2022-08-31) From 9a2a9188c674c41a6a67095c3b102a7dc545fff5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 22 Sep 2022 18:52:52 +0200 Subject: [PATCH 387/662] fix(deps): update dependency puppeteer to v18 (#1471) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8352a339..3a7bfdfe 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^17.0.0", + "puppeteer": "^18.0.0", "sinon": "^14.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index a094933e..14c00a7e 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^8.0.0", - "puppeteer": "^17.0.0" + "puppeteer": "^18.0.0" } } From d8508cb5c9d40695b1ed92fb474db57da18b39c2 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 22 Sep 2022 09:59:53 -0700 Subject: [PATCH 388/662] chore(main): release 8.5.2 (#1458) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 472f4594..93415c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [8.5.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.5.1...v8.5.2) (2022-09-22) + + +### Bug Fixes + +* **deps:** Update dependency puppeteer to v17 ([#1452](https://github.com/googleapis/google-auth-library-nodejs/issues/1452)) ([b317b59](https://github.com/googleapis/google-auth-library-nodejs/commit/b317b5963c18a598fceb85d4f32cc8cd64bb9b7b)) +* **deps:** Update dependency puppeteer to v18 ([#1471](https://github.com/googleapis/google-auth-library-nodejs/issues/1471)) ([9a2a918](https://github.com/googleapis/google-auth-library-nodejs/commit/9a2a9188c674c41a6a67095c3b102a7dc545fff5)) + ## [8.5.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.5.0...v8.5.1) (2022-08-31) diff --git a/package.json b/package.json index 3a7bfdfe..cca4d042 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.5.1", + "version": "8.5.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index bd702a50..741e20f5 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.5.1", + "google-auth-library": "^8.5.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 137883aff56c9e847abb6445c89a76a27536fe26 Mon Sep 17 00:00:00 2001 From: Fedor Isakov Date: Fri, 30 Sep 2022 00:02:36 +0200 Subject: [PATCH 389/662] feat(samples): auth samples (#1444) --- README.md | 6 + samples/README.md | 108 ++++++++++++++++++ samples/authenticateExplicit.js | 73 ++++++++++++ samples/authenticateImplicitWithAdc.js | 60 ++++++++++ samples/idTokenFromImpersonatedCredentials.js | 80 +++++++++++++ samples/idTokenFromMetadataServer.js | 51 +++++++++ samples/idTokenFromServiceAccount.js | 60 ++++++++++ samples/test/auth.test.js | 76 ++++++++++++ samples/verifyGoogleIdToken.js | 69 +++++++++++ 9 files changed, 583 insertions(+) create mode 100644 samples/authenticateExplicit.js create mode 100644 samples/authenticateImplicitWithAdc.js create mode 100644 samples/idTokenFromImpersonatedCredentials.js create mode 100644 samples/idTokenFromMetadataServer.js create mode 100644 samples/idTokenFromServiceAccount.js create mode 100644 samples/test/auth.test.js create mode 100644 samples/verifyGoogleIdToken.js diff --git a/README.md b/README.md index 6cc36cd9..b6264233 100644 --- a/README.md +++ b/README.md @@ -1187,10 +1187,15 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar | Sample | Source Code | Try it | | --------------------------- | --------------------------------- | ------ | | Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/adc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/adc.js,samples/README.md) | +| Authenticate Explicit | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateExplicit.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateExplicit.js,samples/README.md) | +| Authenticate Implicit With Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateImplicitWithAdc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateImplicitWithAdc.js,samples/README.md) | | Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) | | Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/credentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/credentials.js,samples/README.md) | | Downscopedclient | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/downscopedclient.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/downscopedclient.js,samples/README.md) | | Headers | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/headers.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/headers.js,samples/README.md) | +| Id Token From Impersonated Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idTokenFromImpersonatedCredentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idTokenFromImpersonatedCredentials.js,samples/README.md) | +| Id Token From Metadata Server | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idTokenFromMetadataServer.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idTokenFromMetadataServer.js,samples/README.md) | +| Id Token From Service Account | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idTokenFromServiceAccount.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idTokenFromServiceAccount.js,samples/README.md) | | ID Tokens for Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) | | ID Tokens for Serverless | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) | | Jwt | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/jwt.js,samples/README.md) | @@ -1199,6 +1204,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar | Oauth2-code Verifier | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2-codeVerifier.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2-codeVerifier.js,samples/README.md) | | Oauth2 | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2.js,samples/README.md) | | Sign Blob | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlob.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlob.js,samples/README.md) | +| Verify Google Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyGoogleIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyGoogleIdToken.js,samples/README.md) | | Verifying ID Tokens from Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken-iap.js,samples/README.md) | | Verify Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken.js,samples/README.md) | diff --git a/samples/README.md b/samples/README.md index 123cd26c..cf16c381 100644 --- a/samples/README.md +++ b/samples/README.md @@ -13,10 +13,15 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra * [Before you begin](#before-you-begin) * [Samples](#samples) * [Adc](#adc) + * [Authenticate Explicit](#authenticate-explicit) + * [Authenticate Implicit With Adc](#authenticate-implicit-with-adc) * [Compute](#compute) * [Credentials](#credentials) * [Downscopedclient](#downscopedclient) * [Headers](#headers) + * [Id Token From Impersonated Credentials](#id-token-from-impersonated-credentials) + * [Id Token From Metadata Server](#id-token-from-metadata-server) + * [Id Token From Service Account](#id-token-from-service-account) * [ID Tokens for Identity-Aware Proxy (IAP)](#id-tokens-for-identity-aware-proxy-iap) * [ID Tokens for Serverless](#id-tokens-for-serverless) * [Jwt](#jwt) @@ -25,6 +30,7 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra * [Oauth2-code Verifier](#oauth2-code-verifier) * [Oauth2](#oauth2) * [Sign Blob](#sign-blob) + * [Verify Google Id Token](#verify-google-id-token) * [Verifying ID Tokens from Identity-Aware Proxy (IAP)](#verifying-id-tokens-from-identity-aware-proxy-iap) * [Verify Id Token](#verify-id-token) @@ -60,6 +66,40 @@ __Usage:__ +### Authenticate Explicit + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateExplicit.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateExplicit.js,samples/README.md) + +__Usage:__ + + +`node samples/authenticateExplicit.js` + + +----- + + + + +### Authenticate Implicit With Adc + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateImplicitWithAdc.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateImplicitWithAdc.js,samples/README.md) + +__Usage:__ + + +`node samples/authenticateImplicitWithAdc.js` + + +----- + + + + ### Compute View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.js). @@ -128,6 +168,57 @@ __Usage:__ +### Id Token From Impersonated Credentials + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idTokenFromImpersonatedCredentials.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idTokenFromImpersonatedCredentials.js,samples/README.md) + +__Usage:__ + + +`node samples/idTokenFromImpersonatedCredentials.js` + + +----- + + + + +### Id Token From Metadata Server + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idTokenFromMetadataServer.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idTokenFromMetadataServer.js,samples/README.md) + +__Usage:__ + + +`node samples/idTokenFromMetadataServer.js` + + +----- + + + + +### Id Token From Service Account + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idTokenFromServiceAccount.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idTokenFromServiceAccount.js,samples/README.md) + +__Usage:__ + + +`node samples/idTokenFromServiceAccount.js` + + +----- + + + + ### ID Tokens for Identity-Aware Proxy (IAP) Requests an IAP-protected resource with an ID Token. @@ -268,6 +359,23 @@ __Usage:__ +### Verify Google Id Token + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyGoogleIdToken.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyGoogleIdToken.js,samples/README.md) + +__Usage:__ + + +`node samples/verifyGoogleIdToken.js` + + +----- + + + + ### Verifying ID Tokens from Identity-Aware Proxy (IAP) Verifying the signed token from the header of an IAP-protected resource. diff --git a/samples/authenticateExplicit.js b/samples/authenticateExplicit.js new file mode 100644 index 00000000..99986433 --- /dev/null +++ b/samples/authenticateExplicit.js @@ -0,0 +1,73 @@ +// Copyright 2022 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. + +/** + * Lists storage buckets by authenticating with ADC. + */ +function main() { + // [START auth_cloud_explicit_adc] + /** + * TODO(developer): + * 1. Set up ADC as described in https://cloud.google.com/docs/authentication/external/set-up-adc + * 2. Make sure you have the necessary permission to list storage buckets "storage.buckets.list" + */ + + const {GoogleAuth} = require('google-auth-library'); + const {Storage} = require('@google-cloud/storage'); + + async function authenticateExplicit() { + const googleAuth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }); + + // Construct the Google credentials object which obtains the default configuration from your + // working environment. + // googleAuth.getApplicationDefault() will give you ComputeEngineCredentials + // if you are on a GCE (or other metadata server supported environments). + const {credential, projectId} = await googleAuth.getApplicationDefault(); + // If you are authenticating to a Cloud API, you can let the library include the default scope, + // https://www.googleapis.com/auth/cloud-platform, because IAM is used to provide fine-grained + // permissions for Cloud. + // If you need to provide a scope, specify it as follows: + // const googleAuth = new GoogleAuth({ scopes: scope }); + // For more information on scopes to use, + // see: https://developers.google.com/identity/protocols/oauth2/scopes + + const storageOptions = { + projectId, + authClient: credential, + }; + + // Construct the Storage client. + const storage = new Storage(storageOptions); + const [buckets] = await storage.getBuckets(); + console.log('Buckets:'); + + for (const bucket of buckets) { + console.log(`- ${bucket.name}`); + } + + console.log('Listed all storage buckets.'); + } + + authenticateExplicit(); + // [END auth_cloud_explicit_adc] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); + +main(...process.argv.slice(2)); diff --git a/samples/authenticateImplicitWithAdc.js b/samples/authenticateImplicitWithAdc.js new file mode 100644 index 00000000..1d8051d1 --- /dev/null +++ b/samples/authenticateImplicitWithAdc.js @@ -0,0 +1,60 @@ +// Copyright 2022 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. + +/** + * Shows credentials auto-detections in the intercation with GCP libraries + * + * @param {string} projectId - Project ID or project number of the Cloud project you want to use. + */ +function main(projectId) { + // [START auth_cloud_implicit_adc] + /** + * TODO(developer): + * 1. Uncomment and replace these variables before running the sample. + * 2. Set up ADC as described in https://cloud.google.com/docs/authentication/external/set-up-adc + * 3. Make sure you have the necessary permission to list storage buckets "storage.buckets.list" + * (https://cloud.google.com/storage/docs/access-control/iam-permissions#bucket_permissions) + */ + // const projectId = 'YOUR_PROJECT_ID'; + + const {Storage} = require('@google-cloud/storage'); + + async function authenticateImplicitWithAdc() { + // This snippet demonstrates how to list buckets. + // NOTE: Replace the client created below with the client required for your application. + // Note that the credentials are not specified when constructing the client. + // The client library finds your credentials using ADC. + const storage = new Storage({ + projectId, + }); + const [buckets] = await storage.getBuckets(); + console.log('Buckets:'); + + for (const bucket of buckets) { + console.log(`- ${bucket.name}`); + } + + console.log('Listed all storage buckets.'); + } + + authenticateImplicitWithAdc(); + // [END auth_cloud_implicit_adc] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); + +main(...process.argv.slice(2)); diff --git a/samples/idTokenFromImpersonatedCredentials.js b/samples/idTokenFromImpersonatedCredentials.js new file mode 100644 index 00000000..84ea1d2a --- /dev/null +++ b/samples/idTokenFromImpersonatedCredentials.js @@ -0,0 +1,80 @@ +// Copyright 2022 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. + +/** + * Uses a service account (SA1) to impersonate as another service account (SA2) and obtain id token for the impersonated account. + * To obtain token for SA2, SA1 should have the "roles/iam.serviceAccountTokenCreator" permission on SA2. + * + * @param {string} scope - The scope that you might need to request to access Google APIs, + * depending on the level of access you need. For this example, we use the cloud-wide scope + * and use IAM to narrow the permissions: https://cloud.google.com/docs/authentication#authorization_for_services. + * For more information, see: https://developers.google.com/identity/protocols/oauth2/scopes. + * @param {string} targetAudience - The service name for which the id token is requested. Service name refers to the + * logical identifier of an API service, such as "http://www.example.com". + * @param {string} impersonatedServiceAccount - The name of the privilege-bearing service account for whom + * the credential is created. + */ +function main(scope, targetAudience, impersonatedServiceAccount) { + // [START auth_cloud_idtoken_impersonated_credentials] + /** + * TODO(developer): + * 1. Uncomment and replace these variables before running the sample. + */ + // const scope = 'https://www.googleapis.com/auth/cloud-platform'; + // const targetAudience = 'http://www.example.com'; + // const impersonatedServiceAccount = 'name@project.service.gserviceaccount.com'; + + const {GoogleAuth, Impersonated} = require('google-auth-library'); + + async function getIdTokenFromImpersonatedCredentials() { + const googleAuth = new GoogleAuth(); + + // Construct the GoogleCredentials object which obtains the default configuration from your + // working environment. + const {credential} = await googleAuth.getApplicationDefault(); + + // delegates: The chained list of delegates required to grant the final accessToken. + // For more information, see: + // https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-permissions + // Delegate is NOT USED here. + const delegates = []; + + // Create the impersonated credential. + const impersonatedCredentials = new Impersonated({ + sourceClient: credential, + delegates, + targetPrincipal: impersonatedServiceAccount, + targetScopes: [scope], + lifetime: 300, + }); + + // Get the ID token. + // Once you've obtained the ID token, you can use it to make an authenticated call + // to the target audience. + await impersonatedCredentials.fetchIdToken(targetAudience, { + includeEmail: true, + }); + console.log('Generated ID token.'); + } + + getIdTokenFromImpersonatedCredentials(); + // [END auth_cloud_idtoken_impersonated_credentials] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); + +main(...process.argv.slice(2)); diff --git a/samples/idTokenFromMetadataServer.js b/samples/idTokenFromMetadataServer.js new file mode 100644 index 00000000..3454dcc3 --- /dev/null +++ b/samples/idTokenFromMetadataServer.js @@ -0,0 +1,51 @@ +// Copyright 2022 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. + +/** + * Uses the Google Cloud metadata server environment to create an identity token + * and add it to the HTTP request as part of an Authorization header. + * + * @param {string} url - The url or target audience to obtain the ID token for. + */ +function main(url) { + // [START auth_cloud_idtoken_metadata_server] + /** + * TODO(developer): + * 1. Uncomment and replace these variables before running the sample. + */ + // const url = 'http://www.example.com'; + + const {GoogleAuth} = require('google-auth-library'); + + async function getIdTokenFromMetadataServer() { + const googleAuth = new GoogleAuth(); + const client = await googleAuth.getClient(); + + // Get the ID token. + // Once you've obtained the ID token, you can use it to make an authenticated call + // to the target audience. + await client.fetchIdToken(url); + console.log('Generated ID token.'); + } + + getIdTokenFromMetadataServer(); + // [END auth_cloud_idtoken_metadata_server] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); + +main(...process.argv.slice(2)); diff --git a/samples/idTokenFromServiceAccount.js b/samples/idTokenFromServiceAccount.js new file mode 100644 index 00000000..aa8e0224 --- /dev/null +++ b/samples/idTokenFromServiceAccount.js @@ -0,0 +1,60 @@ +// Copyright 2022 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. + +/** + * Obtains the id token by providing the target audience using service account credentials. + * + * @param {string} jsonCredentialsPath - Path to the service account json credential file. + * and use IAM to narrow the permissions: https://cloud.google.com/docs/authentication#authorization_for_services + * @param {string} targetAudience - The url or target audience to obtain the ID token for. + */ +function main(targetAudience, jsonCredentialsPath) { + // [START auth_cloud_idtoken_service_account] + /** + * TODO(developer): + * 1. Uncomment and replace these variables before running the sample. + */ + // const jsonCredentialsPath = '/path/example'; + // const targetAudience = 'http://www.example.com'; + + // Using service account keys introduces risk; they are long-lived, and can be used by anyone + // that obtains the key. Proper rotation and storage reduce this risk but do not eliminate it. + // For these reasons, you should consider an alternative approach that + // does not use a service account key. Several alternatives to service account keys + // are described here: + // https://cloud.google.com/docs/authentication/external/set-up-adc + + const {auth} = require('google-auth-library'); + const jsonConfig = require(jsonCredentialsPath); + + async function getIdTokenFromServiceAccount() { + const client = auth.fromJSON(jsonConfig); + + // Get the ID token. + // Once you've obtained the ID token, use it to make an authenticated call + // to the target audience. + await client.fetchIdToken(targetAudience); + console.log('Generated ID token.'); + } + + getIdTokenFromServiceAccount(); + // [END auth_cloud_idtoken_service_account] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); + +main(...process.argv.slice(2)); diff --git a/samples/test/auth.test.js b/samples/test/auth.test.js new file mode 100644 index 00000000..6188dd2d --- /dev/null +++ b/samples/test/auth.test.js @@ -0,0 +1,76 @@ +// Copyright 2022 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. + +'use strict'; + +const assert = require('assert'); +const cp = require('child_process'); +const {auth} = require('google-auth-library'); +const {describe, it} = require('mocha'); + +const TARGET_AUDIENCE = 'iap.googleapis.com'; +const ZONE = 'us-central1-a'; + +const keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS; + +const execSync = (command, opts) => { + return cp.execSync(command, Object.assign({encoding: 'utf-8'}, opts)); +}; + +describe('auth samples', () => { + it('should authenticate explicitly', async () => { + const output = execSync('node authenticateExplicit'); + + assert.match(output, /Listed all storage buckets./); + }); + + it('should authenticate implicitly with adc', async () => { + const projectId = await auth.getProjectId(); + + const output = execSync( + `node authenticateImplicitWithAdc ${projectId} ${ZONE}` + ); + + assert.match(output, /Listed all storage buckets./); + }); + + it('should get id token from metadata server', async () => { + const output = execSync( + 'node idTokenFromMetadataServer https://www.google.com' + ); + + assert.match(output, /Generated ID token./); + }); + + it('should get id token from service account', async () => { + const output = execSync( + `node idTokenFromServiceAccount ${TARGET_AUDIENCE} ${keyFile}` + ); + + assert.match(output, /Generated ID token./); + }); + + it('should verify google id token', async () => { + const jsonConfig = require(keyFile); + const client = auth.fromJSON(jsonConfig); + + const idToken = await client.fetchIdToken(TARGET_AUDIENCE); + + const output = execSync( + `node verifyGoogleIdToken ${idToken} ${TARGET_AUDIENCE} https://www.googleapis.com/oauth2/v3/certs` + ); + + assert.match(output, /ID token verified./); + }); +}); diff --git a/samples/verifyGoogleIdToken.js b/samples/verifyGoogleIdToken.js new file mode 100644 index 00000000..c01e8ca3 --- /dev/null +++ b/samples/verifyGoogleIdToken.js @@ -0,0 +1,69 @@ +// Copyright 2022 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. + +/** + * Verifies the obtained Google id token. This is done at the receiving end of the OIDC endpoint. + * The most common use case for verifying the ID token is when you are protecting + * your own APIs with IAP. Google services already verify credentials as a platform, + * so verifying ID tokens before making Google API calls is usually unnecessary. + * + * @param {string} idToken - The Google ID token to verify. + * and use IAM to narrow the permissions: https://cloud.google.com/docs/authentication#authorization_for_services + * @param {string} expectedAudience - The service name for which the id token is requested. Service name refers to the + * logical identifier of an API service, such as "iap.googleapis.com". + */ +function main(idToken, expectedAudience) { + // [START auth_cloud_verify_google_idtoken] + /** + * TODO(developer): + * 1. Uncomment and replace these variables before running the sample. + */ + // const idToken = 'id-token'; + // const targetAudience = 'pubsub.googleapis.com'; + + const {OAuth2Client} = require('google-auth-library'); + + async function verifyGoogleIdToken() { + const oAuth2Client = new OAuth2Client(); + + const result = await oAuth2Client.verifyIdToken({ + idToken, + expectedAudience, + }); + + // Verify that the token contains subject and email claims. + // Get the User id. + if (result.payload['sub']) { + console.log(`User id: ${result.payload['sub']}`); + } + + // Optionally, if "includeEmail" was set in the token options, check if the + // email was verified + if (result.payload['email_verified']) { + console.log(`Email verified: ${result.payload['email_verified']}`); + } + + console.log('ID token verified.'); + } + + verifyGoogleIdToken(); + // [END auth_cloud_verify_google_idtoken] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); + +main(...process.argv.slice(2)); From 4bbd13fbf9081e004209d0ffc336648cff0c529e Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Thu, 6 Oct 2022 10:24:54 -0700 Subject: [PATCH 390/662] feat: adding validation for psc endpoints (#1473) * feat: adding validation for psc endpoints * lint fix * adding test cases --- src/auth/baseexternalclient.ts | 8 ++++++++ test/test.baseexternalclient.ts | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index db108566..76841280 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -606,6 +606,14 @@ export abstract class BaseExternalAccountClient extends AuthClient { apiName + GOOGLE_APIS_DOMAIN_PATTERN ), + new RegExp( + '^' + + apiName + + '\\-' + + VARIABLE_PORTION_PATTERN + + '\\.p' + + GOOGLE_APIS_DOMAIN_PATTERN + ), ]; for (const googleAPIsDomainPattern of googleAPIsDomainPatterns) { if (urlDomain.match(googleAPIsDomainPattern)) { diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 5d323003..899015b8 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -176,6 +176,15 @@ describe('BaseExternalAccountClient', () => { 'https://us-east- 1.sts.googleapis.com', 'https://us/.east/.1.sts.googleapis.com', 'https://us.ea\\st.1.sts.googleapis.com', + 'https://sts.pgoogleapis.com', + 'https://p.googleapis.com', + 'https://sts.p.com', + 'http://sts.p.googleapis.com', + 'https://xyz-sts.p.googleapis.com', + 'https://sts-xyz.123.p.googleapis.com', + 'https://sts-xyz.p1.googleapis.com', + 'https://sts-xyz.p.foo.com', + 'https://sts-xyz.p.foo.googleapis.com', ]; invalidTokenUrls.forEach(invalidTokenUrl => { it(`should throw on invalid token url: ${invalidTokenUrl}`, () => { @@ -200,6 +209,9 @@ describe('BaseExternalAccountClient', () => { 'https://us-west-1-sts.googleapis.com', 'https://exmaple.sts.googleapis.com', 'https://example-sts.googleapis.com', + 'https://sts-xyz123.p.googleapis.com', + 'https://sts-xyz-123.p.googleapis.com', + 'https://sts-xys123.p.googleapis.com/path/to/example', ]; const validOptions = Object.assign({}, externalAccountOptions); for (const validTokenUrl of validTokenUrls) { @@ -227,6 +239,15 @@ describe('BaseExternalAccountClient', () => { 'https://us-east- 1.iamcredentials.googleapis.com', 'https://us/.east/.1.iamcredentials.googleapis.com', 'https://us.ea\\st.1.iamcredentials.googleapis.com', + 'https://iamcredentials.pgoogleapis.com', + 'https://p.googleapis.com', + 'https://iamcredentials.p.com', + 'http://iamcredentials.p.googleapis.com', + 'https://xyz-iamcredentials.p.googleapis.com', + 'https://iamcredentials-xyz.123.p.googleapis.com', + 'https://iamcredentials-xyz.p1.googleapis.com', + 'https://iamcredentials-xyz.p.foo.com', + 'https://iamcredentials-xyz.p.foo.googleapis.com', ]; invalidServiceAccountImpersonationUrls.forEach( invalidServiceAccountImpersonationUrl => { @@ -258,6 +279,9 @@ describe('BaseExternalAccountClient', () => { 'https://us-west-1-iamcredentials.googleapis.com', 'https://example.iamcredentials.googleapis.com', 'https://example-iamcredentials.googleapis.com', + 'https://iamcredentials-xyz123.p.googleapis.com', + 'https://iamcredentials-xyz-123.p.googleapis.com', + 'https://iamcredentials-xys123.p.googleapis.com/path/to/example', ]; const validOptions = Object.assign({}, externalAccountOptionsWithSA); for (const validServiceAccountImpersonationUrl of validServiceAccountImpersonationUrls) { From f4bc8feffa1cc8826514d7607c202e5918bdf07f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 19:10:14 +0000 Subject: [PATCH 391/662] chore(main): release 8.6.0 (#1472) :robot: I have created a release *beep* *boop* --- ## [8.6.0](https://togithub.com/googleapis/google-auth-library-nodejs/compare/v8.5.2...v8.6.0) (2022-10-06) ### Features * Adding validation for psc endpoints ([#1473](https://togithub.com/googleapis/google-auth-library-nodejs/issues/1473)) ([4bbd13f](https://togithub.com/googleapis/google-auth-library-nodejs/commit/4bbd13fbf9081e004209d0ffc336648cff0c529e)) * **samples:** Auth samples ([#1444](https://togithub.com/googleapis/google-auth-library-nodejs/issues/1444)) ([137883a](https://togithub.com/googleapis/google-auth-library-nodejs/commit/137883aff56c9e847abb6445c89a76a27536fe26)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93415c5a..84f0e2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [8.6.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.5.2...v8.6.0) (2022-10-06) + + +### Features + +* Adding validation for psc endpoints ([#1473](https://github.com/googleapis/google-auth-library-nodejs/issues/1473)) ([4bbd13f](https://github.com/googleapis/google-auth-library-nodejs/commit/4bbd13fbf9081e004209d0ffc336648cff0c529e)) +* **samples:** Auth samples ([#1444](https://github.com/googleapis/google-auth-library-nodejs/issues/1444)) ([137883a](https://github.com/googleapis/google-auth-library-nodejs/commit/137883aff56c9e847abb6445c89a76a27536fe26)) + ## [8.5.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.5.1...v8.5.2) (2022-09-22) diff --git a/package.json b/package.json index cca4d042..c1b36f92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.5.2", + "version": "8.6.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 741e20f5..3d5aafa1 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.5.2", + "google-auth-library": "^8.6.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 86b258da699810ead624419333a1a7f998f1b376 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 17 Oct 2022 22:04:14 +0200 Subject: [PATCH 392/662] fix(deps): update dependency puppeteer to v19 (#1476) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c1b36f92..ea05e282 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^18.0.0", + "puppeteer": "^19.0.0", "sinon": "^14.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 14c00a7e..7fafb842 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^8.0.0", - "puppeteer": "^18.0.0" + "puppeteer": "^19.0.0" } } From 89daaac8e1230935ed80733ac9c2cd89079e2f04 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 25 Oct 2022 12:10:59 -0700 Subject: [PATCH 393/662] Revert "fix(deps): update dependency puppeteer to v19 (#1476)" (#1480) This reverts commit 86b258da699810ead624419333a1a7f998f1b376. --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ea05e282..c1b36f92 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^19.0.0", + "puppeteer": "^18.0.0", "sinon": "^14.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 7fafb842..14c00a7e 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^8.0.0", - "puppeteer": "^19.0.0" + "puppeteer": "^18.0.0" } } From 6dc4e583dfd3aa3030dfbf959ee1c68a259abe2f Mon Sep 17 00:00:00 2001 From: sai-sunder-s <4540365+sai-sunder-s@users.noreply.github.com> Date: Thu, 3 Nov 2022 18:30:37 +0000 Subject: [PATCH 394/662] fix: Validate url domain for aws metadata urls (#1484) * fix: Validate url domain for aws metadata urls * lint * refactor and add test for ipv6 * update undefined check --- src/auth/awsclient.ts | 23 +++++++++++ test/test.awsclient.ts | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index 7a61fd17..fdbdc411 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -95,6 +95,7 @@ export class AwsClient extends BaseExternalAccountClient { options.credential_source.regional_cred_verification_url; this.imdsV2SessionTokenUrl = options.credential_source.imdsv2_session_token_url; + this.validateMetadataServerUrls(); const match = this.environmentId?.match(/^(aws)(\d+)$/); if (!match || !this.regionalCredVerificationUrl) { throw new Error('No valid AWS "credential_source" provided'); @@ -107,6 +108,28 @@ export class AwsClient extends BaseExternalAccountClient { this.region = ''; } + private validateMetadataServerUrls() { + this.validateMetadataServerUrlIfAny(this.regionUrl, 'region_url'); + this.validateMetadataServerUrlIfAny(this.securityCredentialsUrl, 'url'); + this.validateMetadataServerUrlIfAny( + this.imdsV2SessionTokenUrl, + 'imdsv2_session_token_url' + ); + } + + private validateMetadataServerUrlIfAny( + urlString: string | undefined, + nameOfData: string + ) { + if (urlString !== undefined) { + const url = new URL(urlString); + + if (url.host !== '169.254.169.254' && url.host !== '[fd00:ec2::254]') { + throw new Error(`Invalid host "${url.host}" for "${nameOfData}"`); + } + } + } + /** * Triggered when an external subject token is needed to be exchanged for a * GCP access token via GCP STS endpoint. diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 496d6599..f753930a 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -203,6 +203,63 @@ describe('AwsClient', () => { }); }); + it('should throw when an unsupported url is provided', () => { + const expectedError = new Error('Invalid host "baddomain.com" for "url"'); + const invalidCredentialSource = Object.assign({}, awsCredentialSource); + invalidCredentialSource.url = 'http://baddomain.com/fake'; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + + it('should throw when an unsupported imdsv2_session_token_url is provided', () => { + const expectedError = new Error( + 'Invalid host "baddomain.com" for "imdsv2_session_token_url"' + ); + const invalidCredentialSource = Object.assign( + {imdsv2_session_token_url: 'http://baddomain.com/fake'}, + awsCredentialSource + ); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + + it('should throw when an unsupported region_url is provided', () => { + const expectedError = new Error( + 'Invalid host "baddomain.com" for "region_url"' + ); + const invalidCredentialSource = Object.assign({}, awsCredentialSource); + invalidCredentialSource.region_url = 'http://baddomain.com/fake'; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + it('should throw when an unsupported environment ID is provided', () => { const expectedError = new Error( 'No valid AWS "credential_source" provided' @@ -266,6 +323,39 @@ describe('AwsClient', () => { scope.done(); }); + it('should resolve on success with ipv6', async () => { + const ipv6baseUrl = 'http://[fd00:ec2::254]'; + const ipv6CredentialSource = { + environment_id: 'aws1', + region_url: `${ipv6baseUrl}/latest/meta-data/placement/availability-zone`, + url: `${ipv6baseUrl}/latest/meta-data/iam/security-credentials`, + regional_cred_verification_url: + 'https://sts.{region}.amazonaws.com?' + + 'Action=GetCallerIdentity&Version=2011-06-15', + }; + const ipv6Options = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: ipv6CredentialSource, + }; + + const scope = nock(ipv6baseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials); + + const client = new AwsClient(ipv6Options); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scope.done(); + }); + it('should resolve on success with imdsv2 session token', async () => { const scopes: nock.Scope[] = []; scopes.push( From 8706abc64bb7d7b4336597589abb011150015a8c Mon Sep 17 00:00:00 2001 From: sai-sunder-s <4540365+sai-sunder-s@users.noreply.github.com> Date: Fri, 4 Nov 2022 18:22:54 +0000 Subject: [PATCH 395/662] feat: Introduce environment variable for quota project (#1478) * feat: Introduce environment variable for quota project * remove new line * add awaits * clean up cached cred logic * add explicit quota project * remove explicit value * enhance test Co-authored-by: Daniel Bankhead --- src/auth/authclient.ts | 6 +++++- src/auth/googleauth.ts | 44 +++++++++++++++++++++++++++-------------- test/test.googleauth.ts | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 78882f11..3e443334 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -88,7 +88,11 @@ export abstract class AuthClient extends EventEmitter implements CredentialsClient { - protected quotaProjectId?: string; + /** + * The quota project ID. The quota project can be used by client libraries for the billing purpose. + * See {@link https://cloud.google.com/docs/quota| Working with quotas} + */ + quotaProjectId?: string; transporter = new DefaultTransporter(); credentials: Credentials = {}; projectId?: string | null; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index f288565f..acc9c4f5 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -301,16 +301,18 @@ export class GoogleAuth { private async getApplicationDefaultAsync( options: RefreshOptions = {} ): Promise { - // If we've already got a cached credential, just return it. + // If we've already got a cached credential, return it. + // This will also preserve one's configured quota project, in case they + // set one directly on the credential previously. if (this.cachedCredential) { - return { - credential: this.cachedCredential, - projectId: await this.getProjectIdOptional(), - }; + return await this.prepareAndCacheADC(this.cachedCredential); } + // Since this is a 'new' ADC to cache we will use the environment variable + // if it's available. We prefer this value over the value from ADC. + const quotaProjectIdOverride = process.env['GOOGLE_CLOUD_QUOTA_PROJECT']; + let credential: JSONClient | null; - let projectId: string | null; // 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. @@ -322,10 +324,8 @@ export class GoogleAuth { } else if (credential instanceof BaseExternalAccountClient) { credential.scopes = this.getAnyScopes(); } - this.cachedCredential = credential; - projectId = await this.getProjectIdOptional(); - return {credential, projectId}; + return await this.prepareAndCacheADC(credential, quotaProjectIdOverride); } // Look in the well-known credential file location. @@ -338,9 +338,7 @@ export class GoogleAuth { } else if (credential instanceof BaseExternalAccountClient) { credential.scopes = this.getAnyScopes(); } - this.cachedCredential = credential; - projectId = await this.getProjectIdOptional(); - return {credential, projectId}; + return await this.prepareAndCacheADC(credential, quotaProjectIdOverride); } // Determine if we're running on GCE. @@ -365,9 +363,25 @@ export class GoogleAuth { // For GCE, just return a default ComputeClient. It will take care of // the rest. (options as ComputeOptions).scopes = this.getAnyScopes(); - this.cachedCredential = new Compute(options); - projectId = await this.getProjectIdOptional(); - return {projectId, credential: this.cachedCredential}; + return await this.prepareAndCacheADC( + new Compute(options), + quotaProjectIdOverride + ); + } + + private async prepareAndCacheADC( + credential: JSONClient | Impersonated | Compute | T, + quotaProjectIdOverride?: string + ): Promise { + const projectId = await this.getProjectIdOptional(); + + if (quotaProjectIdOverride) { + credential.quotaProjectId = quotaProjectIdOverride; + } + + this.cachedCredential = credential; + + return {credential, projectId}; } /** diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index ca584254..e5c99424 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -127,6 +127,7 @@ describe('googleauth', () => { GCLOUD_PROJECT: undefined, GOOGLE_APPLICATION_CREDENTIALS: undefined, google_application_credentials: undefined, + GOOGLE_CLOUD_QUOTA_PROJECT: undefined, HOME: path.join('/', 'fake', 'user'), }); sandbox.stub(process, 'env').value(envVars); @@ -1043,6 +1044,40 @@ describe('googleauth', () => { assert.strictEqual(undefined, client.scope); }); + it('explicitly set quota project should not be overriden by environment value', async () => { + mockLinuxWellKnownFile( + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + ); + mockEnvVar('GOOGLE_CLOUD_QUOTA_PROJECT', 'quota_from_env'); + let result = await auth.getApplicationDefault(); + let client = result.credential as JWT; + assert.strictEqual('quota_from_env', client.quotaProjectId); + + client.quotaProjectId = 'explicit_quota'; + result = await auth.getApplicationDefault(); + client = result.credential as JWT; + assert.strictEqual('explicit_quota', client.quotaProjectId); + }); + + it('getApplicationDefault should use quota project id from file if environment variable is empty', async () => { + mockLinuxWellKnownFile( + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + ); + mockEnvVar('GOOGLE_CLOUD_QUOTA_PROJECT', ''); + const result = await auth.getApplicationDefault(); + const client = result.credential as JWT; + assert.strictEqual('my-quota-project', client.quotaProjectId); + }); + + it('getApplicationDefault should use quota project id from file if environment variable is not set', async () => { + mockLinuxWellKnownFile( + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + ); + const result = await auth.getApplicationDefault(); + const client = result.credential as JWT; + assert.strictEqual('my-quota-project', client.quotaProjectId); + }); + it('getApplicationDefault should use GCE when well-known file and env const are not set', async () => { // Set up the creds. // * Environment variable is not set. From 515441fb8899a6eeb4a8fca48d5e19cf41272b2f Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 8 Nov 2022 14:38:20 -0800 Subject: [PATCH 396/662] refactor: Validate AWS Metadata URLs (#1486) Notes: - Validates a URL's `hostname` rather than`host` - Added test for URL with port - Uses static variables for AWS metadata IP Addresses - `RangeError` rather than `Error` - Separated validators from setters in `constructor` --- src/auth/awsclient.ts | 42 ++++++++++++++++++++------------ test/test.awsclient.ts | 55 ++++++++++++++++++++++++------------------ 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index fdbdc411..d6dae618 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -72,6 +72,9 @@ export class AwsClient extends BaseExternalAccountClient { private awsRequestSigner: AwsRequestSigner | null; private region: string; + static AWS_EC2_METADATA_IPV4_ADDRESS = '169.254.169.254'; + static AWS_EC2_METADATA_IPV6_ADDRESS = 'fd00:ec2::254'; + /** * Instantiates an AwsClient instance using the provided JSON * object loaded from an external account credentials file. @@ -95,7 +98,15 @@ export class AwsClient extends BaseExternalAccountClient { options.credential_source.regional_cred_verification_url; this.imdsV2SessionTokenUrl = options.credential_source.imdsv2_session_token_url; - this.validateMetadataServerUrls(); + this.awsRequestSigner = null; + this.region = ''; + + // data validators + this.validateEnvironmentId(); + this.validateMetadataServerURLs(); + } + + private validateEnvironmentId() { const match = this.environmentId?.match(/^(aws)(\d+)$/); if (!match || !this.regionalCredVerificationUrl) { throw new Error('No valid AWS "credential_source" provided'); @@ -104,29 +115,28 @@ export class AwsClient extends BaseExternalAccountClient { `aws version "${match[2]}" is not supported in the current build.` ); } - this.awsRequestSigner = null; - this.region = ''; } - private validateMetadataServerUrls() { - this.validateMetadataServerUrlIfAny(this.regionUrl, 'region_url'); - this.validateMetadataServerUrlIfAny(this.securityCredentialsUrl, 'url'); - this.validateMetadataServerUrlIfAny( + private validateMetadataServerURLs() { + this.validateMetadataURL(this.regionUrl, 'region_url'); + this.validateMetadataURL(this.securityCredentialsUrl, 'url'); + this.validateMetadataURL( this.imdsV2SessionTokenUrl, 'imdsv2_session_token_url' ); } - private validateMetadataServerUrlIfAny( - urlString: string | undefined, - nameOfData: string - ) { - if (urlString !== undefined) { - const url = new URL(urlString); + private validateMetadataURL(value?: string, prop?: string) { + if (!value) return; + const url = new URL(value); - if (url.host !== '169.254.169.254' && url.host !== '[fd00:ec2::254]') { - throw new Error(`Invalid host "${url.host}" for "${nameOfData}"`); - } + if ( + url.hostname !== AwsClient.AWS_EC2_METADATA_IPV4_ADDRESS && + url.hostname !== `[${AwsClient.AWS_EC2_METADATA_IPV6_ADDRESS}]` + ) { + throw new RangeError( + `Invalid host "${url.hostname}" for "${prop}". Expecting ${AwsClient.AWS_EC2_METADATA_IPV4_ADDRESS} or ${AwsClient.AWS_EC2_METADATA_IPV6_ADDRESS}.` + ); } } diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index f753930a..583d8329 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -197,14 +197,31 @@ describe('AwsClient', () => { credential_source: invalidCredentialSource, }; - assert.throws(() => { - return new AwsClient(invalidOptions); - }, expectedError); + assert.throws(() => new AwsClient(invalidOptions), expectedError); }); }); - it('should throw when an unsupported url is provided', () => { - const expectedError = new Error('Invalid host "baddomain.com" for "url"'); + it('should support credential_source with a port number', () => { + const validCredentialSource = {...awsCredentialSource}; + const validURLWithPort = new URL(validCredentialSource.url); + validURLWithPort.port = '8888'; + + validCredentialSource.url = validURLWithPort.href; + const validOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: validCredentialSource, + }; + + assert.doesNotThrow(() => new AwsClient(validOptions)); + }); + + it('should throw when an unsupported credential_source is provided', () => { + const expectedError = new RangeError( + 'Invalid host "baddomain.com" for "url". Expecting 169.254.169.254 or fd00:ec2::254.' + ); const invalidCredentialSource = Object.assign({}, awsCredentialSource); invalidCredentialSource.url = 'http://baddomain.com/fake'; const invalidOptions = { @@ -215,14 +232,12 @@ describe('AwsClient', () => { credential_source: invalidCredentialSource, }; - assert.throws(() => { - return new AwsClient(invalidOptions); - }, expectedError); + assert.throws(() => new AwsClient(invalidOptions), expectedError); }); it('should throw when an unsupported imdsv2_session_token_url is provided', () => { - const expectedError = new Error( - 'Invalid host "baddomain.com" for "imdsv2_session_token_url"' + const expectedError = new RangeError( + 'Invalid host "baddomain.com" for "imdsv2_session_token_url". Expecting 169.254.169.254 or fd00:ec2::254.' ); const invalidCredentialSource = Object.assign( {imdsv2_session_token_url: 'http://baddomain.com/fake'}, @@ -236,14 +251,12 @@ describe('AwsClient', () => { credential_source: invalidCredentialSource, }; - assert.throws(() => { - return new AwsClient(invalidOptions); - }, expectedError); + assert.throws(() => new AwsClient(invalidOptions), expectedError); }); it('should throw when an unsupported region_url is provided', () => { - const expectedError = new Error( - 'Invalid host "baddomain.com" for "region_url"' + const expectedError = new RangeError( + 'Invalid host "baddomain.com" for "region_url". Expecting 169.254.169.254 or fd00:ec2::254.' ); const invalidCredentialSource = Object.assign({}, awsCredentialSource); invalidCredentialSource.region_url = 'http://baddomain.com/fake'; @@ -255,9 +268,7 @@ describe('AwsClient', () => { credential_source: invalidCredentialSource, }; - assert.throws(() => { - return new AwsClient(invalidOptions); - }, expectedError); + assert.throws(() => new AwsClient(invalidOptions), expectedError); }); it('should throw when an unsupported environment ID is provided', () => { @@ -274,9 +285,7 @@ describe('AwsClient', () => { credential_source: invalidCredentialSource, }; - assert.throws(() => { - return new AwsClient(invalidOptions); - }, expectedError); + assert.throws(() => new AwsClient(invalidOptions), expectedError); }); it('should throw when an unsupported environment version is provided', () => { @@ -293,9 +302,7 @@ describe('AwsClient', () => { credential_source: invalidCredentialSource, }; - assert.throws(() => { - return new AwsClient(invalidOptions); - }, expectedError); + assert.throws(() => new AwsClient(invalidOptions), expectedError); }); it('should not throw when valid AWS options are provided', () => { From e53c617b04610d20710356b4c4c8f18959d63563 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 9 Nov 2022 08:03:03 -0800 Subject: [PATCH 397/662] chore(main): release 8.7.0 (#1477) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f0e2d4..44c64c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [8.7.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.6.0...v8.7.0) (2022-11-08) + + +### Features + +* Introduce environment variable for quota project ([#1478](https://github.com/googleapis/google-auth-library-nodejs/issues/1478)) ([8706abc](https://github.com/googleapis/google-auth-library-nodejs/commit/8706abc64bb7d7b4336597589abb011150015a8c)) + + +### Bug Fixes + +* **deps:** Update dependency puppeteer to v19 ([#1476](https://github.com/googleapis/google-auth-library-nodejs/issues/1476)) ([86b258d](https://github.com/googleapis/google-auth-library-nodejs/commit/86b258da699810ead624419333a1a7f998f1b376)) +* Validate url domain for aws metadata urls ([#1484](https://github.com/googleapis/google-auth-library-nodejs/issues/1484)) ([6dc4e58](https://github.com/googleapis/google-auth-library-nodejs/commit/6dc4e583dfd3aa3030dfbf959ee1c68a259abe2f)) + ## [8.6.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.5.2...v8.6.0) (2022-10-06) diff --git a/package.json b/package.json index c1b36f92..c98dc164 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.6.0", + "version": "8.7.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 3d5aafa1..e248078c 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^3.0.0", - "google-auth-library": "^8.6.0", + "google-auth-library": "^8.7.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 22de7040690fd11dca32b6a46585404c97233ff6 Mon Sep 17 00:00:00 2001 From: Patti Shin Date: Fri, 11 Nov 2022 11:19:25 -0800 Subject: [PATCH 398/662] chore(issue-1485): clarify audience + url example usage (#1487) * refactor: updating to clarify audience + url example usage * fix: lint error * Update samples/idtokens-serverless.js Co-authored-by: Daniel Bankhead Co-authored-by: Daniel Bankhead --- samples/idtokens-serverless.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index 2012c3cb..4c59f11f 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -45,13 +45,12 @@ function main( // [END cloudrun_service_to_service_auth] // [START functions_bearer_token] - // For Cloud Functions, `endpoint` and `audience` should be equal. - // Example: https://project-region-projectid.cloudfunctions.net/myFunction - // const url = 'https://TARGET_HOSTNAME/TARGET_URL'; + // Cloud Functions uses your function's url as the `targetAudience` value + // const targetAudience = 'https://project-region-projectid.cloudfunctions.net/myFunction'; + // For Cloud Functions, endpoint (`url`) and `targetAudience` should be equal + // const url = targetAudience; - // Example (Cloud Functions): https://project-region-projectid.cloudfunctions.net/myFunction - // const targetAudience = 'https://TARGET_AUDIENCE/'; // [END functions_bearer_token] // [START functions_bearer_token] From 74a9fffed6b8fb3cdb6e90a26960586baed40f3b Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 23 Nov 2022 13:19:37 -0800 Subject: [PATCH 399/662] refactor: Unify `GoogleAuth#fromJSON` logic + fix sample (#1488) * refactor: Fix `idTokenClient` sample The types were incorrect. * refactor: Unify `GoogleAuth#fromJSON` logic * refactor: remove unused type --- samples/idTokenFromServiceAccount.js | 16 +++++++----- src/auth/googleauth.ts | 38 ++++++++-------------------- src/auth/refreshclient.ts | 2 ++ 3 files changed, 21 insertions(+), 35 deletions(-) diff --git a/samples/idTokenFromServiceAccount.js b/samples/idTokenFromServiceAccount.js index aa8e0224..fb0099a2 100644 --- a/samples/idTokenFromServiceAccount.js +++ b/samples/idTokenFromServiceAccount.js @@ -35,16 +35,18 @@ function main(targetAudience, jsonCredentialsPath) { // are described here: // https://cloud.google.com/docs/authentication/external/set-up-adc - const {auth} = require('google-auth-library'); - const jsonConfig = require(jsonCredentialsPath); + const {GoogleAuth} = require('google-auth-library'); + const credentials = require(jsonCredentialsPath); async function getIdTokenFromServiceAccount() { - const client = auth.fromJSON(jsonConfig); + const auth = new GoogleAuth({credentials}); + + // Get an ID token client. + // The client can be used to make authenticated requests or you can use the + // provider to fetch an id token. + const client = await auth.getIdTokenClient(targetAudience); + await client.idTokenProvider.fetchIdToken(targetAudience); - // Get the ID token. - // Once you've obtained the ID token, use it to make an authenticated call - // to the target audience. - await client.fetchIdToken(targetAudience); console.log('Generated ID token.'); } diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index acc9c4f5..41f2a48e 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -29,7 +29,11 @@ import {IdTokenClient} from './idtokenclient'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; import {Headers, OAuth2ClientOptions, RefreshOptions} from './oauth2client'; -import {UserRefreshClient, UserRefreshClientOptions} from './refreshclient'; +import { + UserRefreshClient, + UserRefreshClientOptions, + USER_REFRESH_ACCOUNT_TYPE, +} from './refreshclient'; import { Impersonated, ImpersonatedOptions, @@ -569,16 +573,12 @@ export class GoogleAuth { */ fromJSON( json: JWTInput | ImpersonatedJWTInput, - options?: RefreshOptions + options: RefreshOptions = {} ): JSONClient { let client: JSONClient; - if (!json) { - throw new Error( - 'Must pass in a JSON object containing the Google auth settings.' - ); - } + options = options || {}; - if (json.type === 'authorized_user') { + if (json.type === USER_REFRESH_ACCOUNT_TYPE) { client = new UserRefreshClient(options); client.fromJSON(json); } else if (json.type === IMPERSONATED_ACCOUNT_TYPE) { @@ -609,26 +609,8 @@ export class GoogleAuth { json: JWTInput, options?: RefreshOptions ): JSONClient { - let client: JSONClient; - // create either a UserRefreshClient or JWT client. - options = options || {}; - if (json.type === 'authorized_user') { - client = new UserRefreshClient(options); - client.fromJSON(json); - } else if (json.type === IMPERSONATED_ACCOUNT_TYPE) { - client = this.fromImpersonatedJSON(json as ImpersonatedJWTInput); - } else if (json.type === EXTERNAL_ACCOUNT_TYPE) { - client = ExternalAccountClient.fromJSON( - json as ExternalAccountClientOptions, - options - )!; - client.scopes = this.getAnyScopes(); - } else { - (options as JWTOptions).scopes = this.scopes; - client = new JWT(options); - this.setGapicJWTValues(client); - client.fromJSON(json); - } + const client = this.fromJSON(json, options); + // cache both raw data used to instantiate client and client itself. this.jsonContent = json; this.cachedCredential = client; diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 91d831bd..e7ca73b3 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -16,6 +16,8 @@ import * as stream from 'stream'; import {JWTInput} from './credentials'; import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; +export const USER_REFRESH_ACCOUNT_TYPE = 'authorized_user'; + export interface UserRefreshClientOptions extends RefreshOptions { clientId?: string; clientSecret?: string; From d4de9412e12f1f6f23f2a7c0d176dc5d2543e607 Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Tue, 29 Nov 2022 13:44:01 -0800 Subject: [PATCH 400/662] fix: do not call metadata server if security creds and region are retrievable through environment vars (#1493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: do not call metadata server if security creds and region are retrievable through environment vars * comments * refactor * review * fix for consistency * lint * remove docs check * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot --- src/auth/awsclient.ts | 71 +++++++++----- src/auth/awsrequestsigner.ts | 2 +- test/test.awsclient.ts | 182 ++++++++++++++++++++++++++++++++--- 3 files changed, 219 insertions(+), 36 deletions(-) diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index d6dae618..80911bb5 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -14,7 +14,7 @@ import {GaxiosOptions} from 'gaxios'; -import {AwsRequestSigner} from './awsrequestsigner'; +import {AwsRequestSigner, AwsSecurityCredentials} from './awsrequestsigner'; import { BaseExternalAccountClient, BaseExternalAccountClientOptions, @@ -48,7 +48,7 @@ export interface AwsClientOptions extends BaseExternalAccountClientOptions { /** * Interface defining the AWS security-credentials endpoint response. */ -interface AwsSecurityCredentials { +interface AwsSecurityCredentialsResponse { Code: string; LastUpdated: string; Type: string; @@ -101,7 +101,7 @@ export class AwsClient extends BaseExternalAccountClient { this.awsRequestSigner = null; this.region = ''; - // data validators + // Data validators. this.validateEnvironmentId(); this.validateMetadataServerURLs(); } @@ -168,7 +168,12 @@ export class AwsClient extends BaseExternalAccountClient { // Initialize AWS request signer if not already initialized. if (!this.awsRequestSigner) { const metadataHeaders: Headers = {}; - if (this.imdsV2SessionTokenUrl) { + // Only retrieve the IMDSv2 session token if both the security credentials and region are + // not retrievable through the environment. + // The credential config contains all the URLs by default but clients may be running this + // where the metadata server is not available and returning the credentials through the environment. + // Removing this check may break them. + if (this.shouldUseMetadataServer() && this.imdsV2SessionTokenUrl) { metadataHeaders['x-aws-ec2-metadata-token'] = await this.getImdsV2SessionToken(); } @@ -177,16 +182,8 @@ export class AwsClient extends BaseExternalAccountClient { this.awsRequestSigner = new AwsRequestSigner(async () => { // Check environment variables for permanent credentials first. // https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html - if ( - process.env['AWS_ACCESS_KEY_ID'] && - process.env['AWS_SECRET_ACCESS_KEY'] - ) { - return { - accessKeyId: process.env['AWS_ACCESS_KEY_ID']!, - secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY']!, - // This is normally not available for permanent credentials. - token: process.env['AWS_SESSION_TOKEN'], - }; + if (this.securityCredentialsFromEnv) { + return this.securityCredentialsFromEnv; } // Since the role on a VM can change, we don't need to cache it. const roleName = await this.getAwsRoleName(metadataHeaders); @@ -273,8 +270,8 @@ export class AwsClient extends BaseExternalAccountClient { private async getAwsRegion(headers: Headers): Promise { // Priority order for region determination: // AWS_REGION > AWS_DEFAULT_REGION > metadata server. - if (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION']) { - return (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'])!; + if (this.regionFromEnv) { + return this.regionFromEnv; } if (!this.regionUrl) { throw new Error( @@ -327,12 +324,42 @@ export class AwsClient extends BaseExternalAccountClient { private async getAwsSecurityCredentials( roleName: string, headers: Headers - ): Promise { - const response = await this.transporter.request({ - url: `${this.securityCredentialsUrl}/${roleName}`, - responseType: 'json', - headers: headers, - }); + ): Promise { + const response = + await this.transporter.request({ + url: `${this.securityCredentialsUrl}/${roleName}`, + responseType: 'json', + headers: headers, + }); return response.data; } + + private shouldUseMetadataServer(): boolean { + // The metadata server must be used when either the AWS region or AWS security + // credentials cannot be retrieved through their defined environment variables. + return !this.regionFromEnv || !this.securityCredentialsFromEnv; + } + + private get regionFromEnv(): string | null { + // The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. + // Only one is required. + return ( + process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'] || null + ); + } + + private get securityCredentialsFromEnv(): AwsSecurityCredentials | null { + // Both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required. + if ( + process.env['AWS_ACCESS_KEY_ID'] && + process.env['AWS_SECRET_ACCESS_KEY'] + ) { + return { + accessKeyId: process.env['AWS_ACCESS_KEY_ID'], + secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'], + token: process.env['AWS_SESSION_TOKEN'], + }; + } + return null; + } } diff --git a/src/auth/awsrequestsigner.ts b/src/auth/awsrequestsigner.ts index a0e8870c..4bb5a151 100644 --- a/src/auth/awsrequestsigner.ts +++ b/src/auth/awsrequestsigner.ts @@ -40,7 +40,7 @@ interface AwsAuthHeaderMap { * These are either determined from AWS security_credentials endpoint or * AWS environment variables. */ -interface AwsSecurityCredentials { +export interface AwsSecurityCredentials { accessKeyId: string; secretAccessKey: string; token?: string; diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 583d8329..04e59866 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -55,6 +55,10 @@ describe('AwsClient', () => { 'https://sts.{region}.amazonaws.com?' + 'Action=GetCallerIdentity&Version=2011-06-15', }; + const awsCredentialSourceWithImdsv2 = Object.assign( + {imdsv2_session_token_url: `${metadataBaseUrl}/latest/api/token`}, + awsCredentialSource + ); const awsOptions = { type: 'external_account', audience, @@ -62,6 +66,13 @@ describe('AwsClient', () => { token_url: getTokenUrl(), credential_source: awsCredentialSource, }; + const awsOptionsWithImdsv2 = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: awsCredentialSourceWithImdsv2, + }; const awsOptionsWithSA = Object.assign( { service_account_impersonation_url: getServiceAccountImpersonationUrl(), @@ -385,19 +396,7 @@ describe('AwsClient', () => { .reply(200, awsSecurityCredentials) ); - const credentialSourceWithSessionTokenUrl = Object.assign( - {imdsv2_session_token_url: `${metadataBaseUrl}/latest/api/token`}, - awsCredentialSource - ); - const awsOptionsWithSessionTokenUrl = { - type: 'external_account', - audience, - subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', - token_url: getTokenUrl(), - credential_source: credentialSourceWithSessionTokenUrl, - }; - - const client = new AwsClient(awsOptionsWithSessionTokenUrl); + const client = new AwsClient(awsOptionsWithImdsv2); const subjectToken = await client.retrieveSubjectToken(); assert.deepEqual(subjectToken, expectedSubjectToken); @@ -829,6 +828,163 @@ describe('AwsClient', () => { assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); }); + + it('should resolve on success for permanent creds with imdsv2', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }) + .put('/latest/api/token') + .reply(200, awsSessionToken) + ); + + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, + }) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + ); + + const client = new AwsClient(awsOptionsWithImdsv2); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + scopes.forEach(scope => scope.done()); + }); + + it('should resolve on success for temporary creds with imdsv2', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + process.env.AWS_SESSION_TOKEN = token; + + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }) + .put('/latest/api/token') + .reply(200, awsSessionToken) + ); + + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, + }) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + ); + + const client = new AwsClient(awsOptionsWithImdsv2); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scopes.forEach(scope => scope.done()); + }); + + it('should not call metadata server with imdsv2 if creds are retrievable through env', async () => { + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + process.env.AWS_REGION = awsRegion; + + const client = new AwsClient(awsOptionsWithImdsv2); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + }); + + it('should call metadata server with imdsv2 if creds are not retrievable through env', async () => { + process.env.AWS_REGION = awsRegion; + + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }) + .put('/latest/api/token') + .reply(200, awsSessionToken) + ); + + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, + }) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials) + ); + + const client = new AwsClient(awsOptionsWithImdsv2); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scopes.forEach(scope => scope.done()); + }); + + it('should call metadata server with imdsv2 if secret access key is not not retrievable through env', async () => { + process.env.AWS_REGION = awsRegion; + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }) + .put('/latest/api/token') + .reply(200, awsSessionToken) + ); + + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, + }) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials) + ); + + const client = new AwsClient(awsOptionsWithImdsv2); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scopes.forEach(scope => scope.done()); + }); + + it('should call metadata server with imdsv2 if access key is not not retrievable through env', async () => { + process.env.AWS_DEFAULT_REGION = awsRegion; + process.env.AWS_SECRET_ACCESS_KEY = accessKeyId; + + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }) + .put('/latest/api/token') + .reply(200, awsSessionToken) + ); + + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, + }) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials) + ); + + const client = new AwsClient(awsOptionsWithImdsv2); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scopes.forEach(scope => scope.done()); + }); }); describe('getAccessToken()', () => { From 00d6135f35a1aa193d50fad6b3ec28a7fda9df66 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 29 Nov 2022 23:04:06 +0100 Subject: [PATCH 401/662] fix(deps): update dependency @googleapis/iam to v4 (#1482) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index e248078c..00b80b6c 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^6.0.0", - "@googleapis/iam": "^3.0.0", + "@googleapis/iam": "^4.0.0", "google-auth-library": "^8.7.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 337c6ace5e8f88466e061e3f2ea17c1a3775a9a4 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 29 Nov 2022 23:48:23 +0100 Subject: [PATCH 402/662] chore(deps): update dependency sinon to v15 (#1495) Co-authored-by: Daniel Bankhead --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c98dc164..309ee64b 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "nock": "^13.0.0", "null-loader": "^4.0.0", "puppeteer": "^18.0.0", - "sinon": "^14.0.0", + "sinon": "^15.0.0", "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "^4.6.3", From fb40dd7be53dbeb34a10521050fb14e4cc44e6e9 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 29 Nov 2022 15:34:07 -0800 Subject: [PATCH 403/662] refactor(test): Use Native FS Temp Directory Tools (#1494) * test: Use Native FS Temp Directory Tools Fixes: #1481 * build: fix linkinator config fo 169.254.169.254 * revert: linkinator debug --- package.json | 2 -- system-test/test.kitchen.ts | 37 +++++++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 309ee64b..2e5d3f42 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "@types/ncp": "^2.0.1", "@types/node": "^16.0.0", "@types/sinon": "^10.0.0", - "@types/tmp": "^0.2.0", "assert-rejects": "^1.0.0", "c8": "^7.0.0", "chai": "^4.2.0", @@ -62,7 +61,6 @@ "null-loader": "^4.0.0", "puppeteer": "^18.0.0", "sinon": "^15.0.0", - "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "^4.6.3", "webpack": "^5.21.2", diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index f9698772..3111533b 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -18,8 +18,8 @@ import * as execa from 'execa'; import * as fs from 'fs'; import * as mv from 'mv'; import {ncp} from 'ncp'; +import * as os from 'os'; import * as path from 'path'; -import * as tmp from 'tmp'; import {promisify} from 'util'; const mvp = promisify(mv) as {} as (...args: string[]) => Promise; @@ -28,18 +28,19 @@ const keep = !!process.env.GALN_KEEP_TEMPDIRS; // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../package.json'); -let stagingDir: tmp.DirResult; -let stagingPath: string; +let stagingDir: string; async function packAndInstall() { - stagingDir = tmp.dirSync({keep, unsafeCleanup: true}); - stagingPath = stagingDir.name; + stagingDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'google-auth-library-nodejs-pack-') + ); + await execa('npm', ['pack'], {stdio: 'inherit'}); const tarball = `${pkg.name}-${pkg.version}.tgz`; // stagingPath can be on another filesystem so fs.rename() will fail // with EXDEV, hence we use `mv` module here. - await mvp(tarball, `${stagingPath}/google-auth-library.tgz`); - await ncpp('system-test/fixtures/kitchen', `${stagingPath}/`); - await execa('npm', ['install'], {cwd: `${stagingPath}/`, stdio: 'inherit'}); + await mvp(tarball, `${stagingDir}/google-auth-library.tgz`); + await ncpp('system-test/fixtures/kitchen', `${stagingDir}/`); + await execa('npm', ['install'], {cwd: `${stagingDir}/`, stdio: 'inherit'}); } describe('pack and install', () => { @@ -60,8 +61,8 @@ describe('pack and install', () => { this.timeout(40000); await packAndInstall(); // we expect npm install is executed in the before hook - await execa('npx', ['webpack'], {cwd: `${stagingPath}/`, stdio: 'inherit'}); - const bundle = path.join(stagingPath, 'dist', 'bundle.min.js'); + await execa('npx', ['webpack'], {cwd: `${stagingDir}/`, stdio: 'inherit'}); + const bundle = path.join(stagingDir, 'dist', 'bundle.min.js'); const stat = fs.statSync(bundle); assert(stat.size < 256 * 1024); }); @@ -69,9 +70,21 @@ describe('pack and install', () => { /** * CLEAN UP - remove the staging directory when done. */ - afterEach('cleanup staging', () => { + afterEach('cleanup staging', async () => { if (!keep) { - stagingDir.removeCallback(); + if ('rm' in fs.promises) { + await fs.promises.rm(stagingDir, { + force: true, + recursive: true, + }); + } else { + // Must be on Node 14-. + // Here, `rmdir` can also delete files. + // Background: https://github.com/nodejs/node/issues/34278 + await fs.promises.rmdir(stagingDir, { + recursive: true, + }); + } } }); }); From 02f8fe96b3231df7d44f1e47521a8e0205d85a67 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 16:44:18 -0800 Subject: [PATCH 404/662] build: have Kokoro grab service account credentials from secret that will be rotated (#1498) * build: have Kokoro grab service account credentials from secret that will be rotated * build: have Kokoro grab service account credentials from secret that will be rotated Source-Link: https://github.com/googleapis/synthtool/commit/4a0230eb8dc497f36fd3839e6144982131f30a9d Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:f59941869d508c6825deeffce180579545fd528f359f549a80a18ec0458d7094 * Update idTokenFromServiceAccount.js * build: update sample test * fix json path * build: update samples test Co-authored-by: Owl Bot Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> Co-authored-by: Sofia Leon --- .github/.OwlBot.lock.yaml | 3 +-- .kokoro/continuous/node12/samples-test.cfg | 5 +++++ .kokoro/presubmit/node12/samples-test.cfg | 5 +++++ .kokoro/samples-test.sh | 2 +- samples/idTokenFromServiceAccount.js | 3 ++- samples/jwt.js | 3 ++- samples/test/auth.test.js | 3 ++- 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index cb86baf8..6c41b308 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,4 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:7fefeb9e517db2dd8c8202d9239ff6788d6852bc92dd3aac57a46059679ac9de -# created: 2022-08-24T19:44:03.464675104Z + digest: sha256:f59941869d508c6825deeffce180579545fd528f359f549a80a18ec0458d7094 diff --git a/.kokoro/continuous/node12/samples-test.cfg b/.kokoro/continuous/node12/samples-test.cfg index 308a084d..9571f5db 100644 --- a/.kokoro/continuous/node12/samples-test.cfg +++ b/.kokoro/continuous/node12/samples-test.cfg @@ -5,3 +5,8 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/google-auth-library-nodejs/.kokoro/samples-test.sh" } + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node12/samples-test.cfg b/.kokoro/presubmit/node12/samples-test.cfg index 308a084d..9571f5db 100644 --- a/.kokoro/presubmit/node12/samples-test.cfg +++ b/.kokoro/presubmit/node12/samples-test.cfg @@ -5,3 +5,8 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/google-auth-library-nodejs/.kokoro/samples-test.sh" } + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index fbc058a4..806c0082 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -19,7 +19,7 @@ set -eo pipefail export NPM_CONFIG_PREFIX=${HOME}/.npm-global # Setup service account credentials. -export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json +export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/secret_manager/long-door-651-kokoro-system-test-service-account export GCLOUD_PROJECT=long-door-651 cd $(dirname $0)/.. diff --git a/samples/idTokenFromServiceAccount.js b/samples/idTokenFromServiceAccount.js index fb0099a2..01621584 100644 --- a/samples/idTokenFromServiceAccount.js +++ b/samples/idTokenFromServiceAccount.js @@ -36,7 +36,8 @@ function main(targetAudience, jsonCredentialsPath) { // https://cloud.google.com/docs/authentication/external/set-up-adc const {GoogleAuth} = require('google-auth-library'); - const credentials = require(jsonCredentialsPath); + const fs = require('fs'); + const credentials = JSON.parse(fs.readFileSync(jsonCredentialsPath, 'utf8')); async function getIdTokenFromServiceAccount() { const auth = new GoogleAuth({credentials}); diff --git a/samples/jwt.js b/samples/jwt.js index e5530e4e..3110c8f9 100644 --- a/samples/jwt.js +++ b/samples/jwt.js @@ -24,12 +24,13 @@ **/ const {JWT} = require('google-auth-library'); +const fs = require('fs'); async function main( // Full path to the sevice account credential keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS ) { - const keys = require(keyFile); + const keys = JSON.parse(fs.readFileSync(keyFile, 'utf8')); const client = new JWT({ email: keys.client_email, key: keys.private_key, diff --git a/samples/test/auth.test.js b/samples/test/auth.test.js index 6188dd2d..7473fc4d 100644 --- a/samples/test/auth.test.js +++ b/samples/test/auth.test.js @@ -18,6 +18,7 @@ const assert = require('assert'); const cp = require('child_process'); const {auth} = require('google-auth-library'); const {describe, it} = require('mocha'); +const fs = require('fs'); const TARGET_AUDIENCE = 'iap.googleapis.com'; const ZONE = 'us-central1-a'; @@ -62,7 +63,7 @@ describe('auth samples', () => { }); it('should verify google id token', async () => { - const jsonConfig = require(keyFile); + const jsonConfig = JSON.parse(fs.readFileSync(keyFile, 'utf8')); const client = auth.fromJSON(jsonConfig); const idToken = await client.fetchIdToken(TARGET_AUDIENCE); From c0b8c120d87f7a47b8d2d69a129d727fa197356d Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 17:04:00 -0800 Subject: [PATCH 405/662] build: have Kokoro grab service account credentials from secret that will be rotated for system tests (#1507) Source-Link: https://github.com/googleapis/synthtool/commit/abbc97db69a57dcb991ba97ef503305b701ffb3a Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:fe04ae044dadf5ad88d979dbcc85e0e99372fb5d6316790341e6aca5e4e3fbc8 Co-authored-by: Owl Bot Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 2 +- .kokoro/continuous/node12/system-test.cfg | 5 +++++ .kokoro/presubmit/node12/system-test.cfg | 5 +++++ .kokoro/system-test.sh | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 6c41b308..788f7a9f 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,4 +13,4 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:f59941869d508c6825deeffce180579545fd528f359f549a80a18ec0458d7094 + digest: sha256:fe04ae044dadf5ad88d979dbcc85e0e99372fb5d6316790341e6aca5e4e3fbc8 diff --git a/.kokoro/continuous/node12/system-test.cfg b/.kokoro/continuous/node12/system-test.cfg index bb5fd2ad..83d64098 100644 --- a/.kokoro/continuous/node12/system-test.cfg +++ b/.kokoro/continuous/node12/system-test.cfg @@ -5,3 +5,8 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/google-auth-library-nodejs/.kokoro/system-test.sh" } + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node12/system-test.cfg b/.kokoro/presubmit/node12/system-test.cfg index bb5fd2ad..83d64098 100644 --- a/.kokoro/presubmit/node12/system-test.cfg +++ b/.kokoro/presubmit/node12/system-test.cfg @@ -5,3 +5,8 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/google-auth-library-nodejs/.kokoro/system-test.sh" } + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 87fa0653..0201e9df 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -19,7 +19,7 @@ set -eo pipefail export NPM_CONFIG_PREFIX=${HOME}/.npm-global # Setup service account credentials. -export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json +export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/secret_manager/long-door-651-kokoro-system-test-service-account export GCLOUD_PROJECT=long-door-651 cd $(dirname $0)/.. From bdc6339014ae13945bbf82576f7ff71534851ab1 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 11 Jan 2023 07:30:53 -0800 Subject: [PATCH 406/662] test(docs): skip linkinator check (#1511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(docs): skip linkinator check * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * build: fix ignore Co-authored-by: Owl Bot --- .github/workflows/ci.yaml | 11 ++--------- owlbot.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 owlbot.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f447b84a..2377ffbe 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,12 +49,5 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 14 - - run: npm install - - run: npm run docs - - uses: JustinBeckwith/linkinator-action@v1 - with: - paths: docs/ + # See: https://github.com/JustinBeckwith/linkinator-action/issues/148 + - run: echo "temporarily skipping until linkinator's fixed upstream" diff --git a/owlbot.py b/owlbot.py new file mode 100644 index 00000000..2ff4b9eb --- /dev/null +++ b/owlbot.py @@ -0,0 +1,26 @@ +# Copyright 2022 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. +"""This script is used to synthesize generated parts of this library.""" + +import synthtool.languages.node as node +import logging + +logging.basicConfig(level=logging.DEBUG) + +# List of excludes for the enhanced library +node.owlbot_main( + templates_excludes=[ + ".github/workflows/ci.yaml", + ], +) From a278d19a0b211b13e5cf5176d40128e704b55780 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Fri, 3 Feb 2023 13:42:59 -0800 Subject: [PATCH 407/662] fix: Removing 3pi config URL validation (#1517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Removing url validation * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: minor change to language in documentation * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 3 + README.md | 3 + src/auth/baseexternalclient.ts | 80 ------------------- test/test.baseexternalclient.ts | 133 -------------------------------- 4 files changed, 6 insertions(+), 213 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index f73558b1..3284dc89 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -888,6 +888,9 @@ body: |- } ``` + #### Security Considerations + Note that this library does not perform any validation on the token_url, token_info_url, or service_account_impersonation_url fields of the credential configuration. It is not recommended to use a credential configuration that you did not generate with the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain. + ## Working with ID Tokens ### Fetching ID Tokens If your application is running on Cloud Run or Cloud Functions, or using Cloud Identity-Aware diff --git a/README.md b/README.md index b6264233..13539195 100644 --- a/README.md +++ b/README.md @@ -932,6 +932,9 @@ async function main() { } ``` +#### Security Considerations +Note that this library does not perform any validation on the token_url, token_info_url, or service_account_impersonation_url fields of the credential configuration. It is not recommended to use a credential configuration that you did not generate with the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain. + ## Working with ID Tokens ### Fetching ID Tokens If your application is running on Cloud Run or Cloud Functions, or using Cloud Identity-Aware diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 76841280..ddd79d8e 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -37,10 +37,6 @@ const STS_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'; const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; /** The default OAuth scope to request when none is provided. */ const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'; -/** The google apis domain pattern. */ -const GOOGLE_APIS_DOMAIN_PATTERN = '\\.googleapis\\.com$'; -/** The variable portion pattern in a Google APIs domain. */ -const VARIABLE_PORTION_PATTERN = '[^\\.\\s\\/\\\\]+'; /** Default impersonated token lifespan in seconds.*/ const DEFAULT_TOKEN_LIFESPAN = 3600; @@ -171,9 +167,6 @@ export abstract class BaseExternalAccountClient extends AuthClient { clientSecret: options.client_secret, } as ClientAuthentication) : undefined; - if (!this.validateGoogleAPIsUrl('sts', options.token_url)) { - throw new Error(`"${options.token_url}" is not a valid token url.`); - } this.stsCredential = new sts.StsCredentials( options.token_url, this.clientAuth @@ -195,18 +188,6 @@ export abstract class BaseExternalAccountClient extends AuthClient { 'credentials.' ); } - if ( - typeof options.service_account_impersonation_url !== 'undefined' && - !this.validateGoogleAPIsUrl( - 'iamcredentials', - options.service_account_impersonation_url - ) - ) { - throw new Error( - `"${options.service_account_impersonation_url}" is ` + - 'not a valid service account impersonation url.' - ); - } this.serviceAccountImpersonationUrl = options.service_account_impersonation_url; this.serviceAccountImpersonationLifetime = @@ -561,65 +542,4 @@ export abstract class BaseExternalAccountClient extends AuthClient { return this.scopes; } } - - /** - * Checks whether Google APIs URL is valid. - * @param apiName The apiName of url. - * @param url The Google API URL to validate. - * @return Whether the URL is valid or not. - */ - private validateGoogleAPIsUrl(apiName: string, url: string): boolean { - let parsedUrl; - // Return false if error is thrown during parsing URL. - try { - parsedUrl = new URL(url); - } catch (e) { - return false; - } - - const urlDomain = parsedUrl.hostname; - // Check the protocol is https. - if (parsedUrl.protocol !== 'https:') { - return false; - } - - const googleAPIsDomainPatterns: RegExp[] = [ - new RegExp( - '^' + - VARIABLE_PORTION_PATTERN + - '\\.' + - apiName + - GOOGLE_APIS_DOMAIN_PATTERN - ), - new RegExp('^' + apiName + GOOGLE_APIS_DOMAIN_PATTERN), - new RegExp( - '^' + - apiName + - '\\.' + - VARIABLE_PORTION_PATTERN + - GOOGLE_APIS_DOMAIN_PATTERN - ), - new RegExp( - '^' + - VARIABLE_PORTION_PATTERN + - '\\-' + - apiName + - GOOGLE_APIS_DOMAIN_PATTERN - ), - new RegExp( - '^' + - apiName + - '\\-' + - VARIABLE_PORTION_PATTERN + - '\\.p' + - GOOGLE_APIS_DOMAIN_PATTERN - ), - ]; - for (const googleAPIsDomainPattern of googleAPIsDomainPatterns) { - if (urlDomain.match(googleAPIsDomainPattern)) { - return true; - } - } - return false; - } } diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 899015b8..6c98df7d 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -160,139 +160,6 @@ describe('BaseExternalAccountClient', () => { }, expectedError); }); - const invalidTokenUrls = [ - 'http://sts.googleapis.com', - 'https://', - 'https://sts.google.com', - 'https://sts.googleapis.net', - 'https://sts.googleapis.comevil.com', - 'https://sts.googleapis.com.evil.com', - 'https://sts.googleapis.com.evil.com/path/to/example', - 'https://sts..googleapis.com', - 'https://-sts.googleapis.com', - 'https://evilsts.googleapis.com', - 'https://us.east.1.sts.googleapis.com', - 'https://us east 1.sts.googleapis.com', - 'https://us-east- 1.sts.googleapis.com', - 'https://us/.east/.1.sts.googleapis.com', - 'https://us.ea\\st.1.sts.googleapis.com', - 'https://sts.pgoogleapis.com', - 'https://p.googleapis.com', - 'https://sts.p.com', - 'http://sts.p.googleapis.com', - 'https://xyz-sts.p.googleapis.com', - 'https://sts-xyz.123.p.googleapis.com', - 'https://sts-xyz.p1.googleapis.com', - 'https://sts-xyz.p.foo.com', - 'https://sts-xyz.p.foo.googleapis.com', - ]; - invalidTokenUrls.forEach(invalidTokenUrl => { - it(`should throw on invalid token url: ${invalidTokenUrl}`, () => { - const invalidOptions = Object.assign({}, externalAccountOptions); - invalidOptions.token_url = invalidTokenUrl; - const expectedError = new Error( - `"${invalidTokenUrl}" is not a valid token url.` - ); - assert.throws(() => { - return new TestExternalAccountClient(invalidOptions); - }, expectedError); - }); - }); - - it('should not throw on valid token urls', () => { - const validTokenUrls = [ - 'https://sts.googleapis.com', - 'https://sts.us-west-1.googleapis.com', - 'https://sts.google.googleapis.com', - 'https://sts.googleapis.com/path/to/example', - 'https://us-west-1.sts.googleapis.com', - 'https://us-west-1-sts.googleapis.com', - 'https://exmaple.sts.googleapis.com', - 'https://example-sts.googleapis.com', - 'https://sts-xyz123.p.googleapis.com', - 'https://sts-xyz-123.p.googleapis.com', - 'https://sts-xys123.p.googleapis.com/path/to/example', - ]; - const validOptions = Object.assign({}, externalAccountOptions); - for (const validTokenUrl of validTokenUrls) { - validOptions.token_url = validTokenUrl; - assert.doesNotThrow(() => { - return new TestExternalAccountClient(validOptions); - }); - } - }); - - const invalidServiceAccountImpersonationUrls = [ - 'http://iamcredentials.googleapis.com', - 'https://', - 'https://iamcredentials.google.com', - 'https://iamcredentials.googleapis.net', - 'https://iamcredentials.googleapis.comevil.com', - 'https://iamcredentials.googleapis.com.evil.com', - 'https://iamcredentials.googleapis.com.evil.com/path/to/example', - 'https://iamcredentials..googleapis.com', - 'https://-iamcredentials.googleapis.com', - 'https://eviliamcredentials.googleapis.com', - 'https://evil.eviliamcredentials.googleapis.com', - 'https://us.east.1.iamcredentials.googleapis.com', - 'https://us east 1.iamcredentials.googleapis.com', - 'https://us-east- 1.iamcredentials.googleapis.com', - 'https://us/.east/.1.iamcredentials.googleapis.com', - 'https://us.ea\\st.1.iamcredentials.googleapis.com', - 'https://iamcredentials.pgoogleapis.com', - 'https://p.googleapis.com', - 'https://iamcredentials.p.com', - 'http://iamcredentials.p.googleapis.com', - 'https://xyz-iamcredentials.p.googleapis.com', - 'https://iamcredentials-xyz.123.p.googleapis.com', - 'https://iamcredentials-xyz.p1.googleapis.com', - 'https://iamcredentials-xyz.p.foo.com', - 'https://iamcredentials-xyz.p.foo.googleapis.com', - ]; - invalidServiceAccountImpersonationUrls.forEach( - invalidServiceAccountImpersonationUrl => { - it(`should throw on invalid service account impersonation url: ${invalidServiceAccountImpersonationUrl}`, () => { - const invalidOptions = Object.assign( - {}, - externalAccountOptionsWithSA - ); - invalidOptions.service_account_impersonation_url = - invalidServiceAccountImpersonationUrl; - const expectedError = new Error( - `"${invalidServiceAccountImpersonationUrl}" is ` + - 'not a valid service account impersonation url.' - ); - assert.throws(() => { - return new TestExternalAccountClient(invalidOptions); - }, expectedError); - }); - } - ); - - it('should not throw on valid service account impersonation url', () => { - const validServiceAccountImpersonationUrls = [ - 'https://iamcredentials.googleapis.com', - 'https://iamcredentials.us-west-1.googleapis.com', - 'https://iamcredentials.google.googleapis.com', - 'https://iamcredentials.googleapis.com/path/to/example', - 'https://us-west-1.iamcredentials.googleapis.com', - 'https://us-west-1-iamcredentials.googleapis.com', - 'https://example.iamcredentials.googleapis.com', - 'https://example-iamcredentials.googleapis.com', - 'https://iamcredentials-xyz123.p.googleapis.com', - 'https://iamcredentials-xyz-123.p.googleapis.com', - 'https://iamcredentials-xys123.p.googleapis.com/path/to/example', - ]; - const validOptions = Object.assign({}, externalAccountOptionsWithSA); - for (const validServiceAccountImpersonationUrl of validServiceAccountImpersonationUrls) { - validOptions.service_account_impersonation_url = - validServiceAccountImpersonationUrl; - assert.doesNotThrow(() => { - return new TestExternalAccountClient(validOptions); - }); - } - }); - const invalidWorkforceAudiences = [ '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/provider', '//iam.googleapis.com/locations/global/workforcepools/pool/providers/provider', From ff973a68d5666d40d19be7a7bb8a20d0935ac5d6 Mon Sep 17 00:00:00 2001 From: Eisuke Esaki <33922926+eisukeesaki@users.noreply.github.com> Date: Wed, 8 Feb 2023 16:18:34 -0800 Subject: [PATCH 408/662] chore: typo in README (#1519) (#1520) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13539195..42095832 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This library provides a variety of ways to authenticate to your Google services. - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials -This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. +This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. From 9781790f8339851078957fcd55f9f1eb9fb6fc43 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 9 Feb 2023 23:05:10 +0000 Subject: [PATCH 409/662] chore(deps): update dependency karma-sourcemap-loader to ^0.4.0 (#1521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update dependency karma-sourcemap-loader to ^0.4.0 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42095832..13539195 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This library provides a variety of ways to authenticate to your Google services. - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials -This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. +This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. diff --git a/package.json b/package.json index 2e5d3f42..8f05dded 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "karma-coverage": "^2.0.0", "karma-firefox-launcher": "^2.0.0", "karma-mocha": "^2.0.0", - "karma-sourcemap-loader": "^0.3.7", + "karma-sourcemap-loader": "^0.4.0", "karma-webpack": "^5.0.0", "keypair": "^1.0.4", "linkinator": "^4.0.0", From a1f9835fe155722206046d6bb5b56f9e53f2fe9a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 2 Mar 2023 14:37:31 +0000 Subject: [PATCH 410/662] fix(deps): update dependency @googleapis/iam to v5 (#1526) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 00b80b6c..577f5037 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^6.0.0", - "@googleapis/iam": "^4.0.0", + "@googleapis/iam": "^5.0.0", "google-auth-library": "^8.7.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 2562d1192e0f89a3232897b8e27f24a14d5222f2 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 7 Mar 2023 14:15:19 -0800 Subject: [PATCH 411/662] fix(deps): Update `gcp-metadata` to v5.2.0 (#1502) * fix(deps): Update `gcp-metadata` to v5.1.0 * feat: associate `DefaultTransporter` to `Transporter` * chore: update package.json --------- Co-authored-by: danieljbruce --- package.json | 2 +- src/transporters.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8f05dded..096d13a5 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^5.0.0", - "gcp-metadata": "^5.0.0", + "gcp-metadata": "^5.2.0", "gtoken": "^6.1.0", "jws": "^4.0.0", "lru-cache": "^6.0.0" diff --git a/src/transporters.ts b/src/transporters.ts index 952f22af..33dca5ff 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -44,7 +44,7 @@ export interface RequestError extends GaxiosError { errors: Error[]; } -export class DefaultTransporter { +export class DefaultTransporter implements Transporter { /** * Default user agent. */ From 6e504a053d722cffe85434d31f00cc568b87e73f Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 16 Mar 2023 11:51:06 -0700 Subject: [PATCH 412/662] chore: store nodejs build artifacts in placer (#1532) Source-Link: https://github.com/googleapis/synthtool/commit/3602660ae703daadcb7bc2f87bf601241665f3f8 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:e6d785d6de3cab027f6213d95ccedab4cab3811b0d3172b78db2216faa182e32 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/publish.sh | 14 +++++++++++++- .kokoro/release/publish.cfg | 12 ++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 788f7a9f..0b836e11 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,4 +13,4 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:fe04ae044dadf5ad88d979dbcc85e0e99372fb5d6316790341e6aca5e4e3fbc8 + digest: sha256:e6d785d6de3cab027f6213d95ccedab4cab3811b0d3172b78db2216faa182e32 diff --git a/.kokoro/publish.sh b/.kokoro/publish.sh index 949e3e1d..ca1d47af 100755 --- a/.kokoro/publish.sh +++ b/.kokoro/publish.sh @@ -27,4 +27,16 @@ NPM_TOKEN=$(cat $KOKORO_KEYSTORE_DIR/73713_google-cloud-npm-token-1) echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_TOKEN}" > ~/.npmrc npm install -npm publish --access=public --registry=https://wombat-dressing-room.appspot.com +npm pack . +# npm provides no way to specify, observe, or predict the name of the tarball +# file it generates. We have to look in the current directory for the freshest +# .tgz file. +TARBALL=$(ls -1 -t *.tgz | head -1) + +npm publish --access=public --registry=https://wombat-dressing-room.appspot.com "$TARBALL" + +# Kokoro collects *.tgz and package-lock.json files and stores them in Placer +# so we can generate SBOMs and attestations. +# However, we *don't* want Kokoro to collect package-lock.json and *.tgz files +# that happened to be installed with dependencies. +find node_modules -name package-lock.json -o -name "*.tgz" | xargs rm -f \ No newline at end of file diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index 90b0bc09..2479b117 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -37,3 +37,15 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/google-auth-library-nodejs/.kokoro/publish.sh" } + +# Store the packages we uploaded to npmjs.org and their corresponding +# package-lock.jsons in Placer. That way, we have a record of exactly +# what we published, and which version of which tools we used to publish +# it, which we can use to generate SBOMs and attestations. +action { + define_artifacts { + regex: "github/**/*.tgz" + regex: "github/**/package-lock.json" + strip_prefix: "github" + } +} From f4d933579cb5b9e50adf6e679a73cc78388ad8f8 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Tue, 21 Mar 2023 11:20:14 -0700 Subject: [PATCH 413/662] fix: removing aws url validation (#1531) Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> --- src/auth/awsclient.ts | 24 ------------------- test/test.awsclient.ts | 53 ------------------------------------------ 2 files changed, 77 deletions(-) diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index 80911bb5..78575e07 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -103,7 +103,6 @@ export class AwsClient extends BaseExternalAccountClient { // Data validators. this.validateEnvironmentId(); - this.validateMetadataServerURLs(); } private validateEnvironmentId() { @@ -117,29 +116,6 @@ export class AwsClient extends BaseExternalAccountClient { } } - private validateMetadataServerURLs() { - this.validateMetadataURL(this.regionUrl, 'region_url'); - this.validateMetadataURL(this.securityCredentialsUrl, 'url'); - this.validateMetadataURL( - this.imdsV2SessionTokenUrl, - 'imdsv2_session_token_url' - ); - } - - private validateMetadataURL(value?: string, prop?: string) { - if (!value) return; - const url = new URL(value); - - if ( - url.hostname !== AwsClient.AWS_EC2_METADATA_IPV4_ADDRESS && - url.hostname !== `[${AwsClient.AWS_EC2_METADATA_IPV6_ADDRESS}]` - ) { - throw new RangeError( - `Invalid host "${url.hostname}" for "${prop}". Expecting ${AwsClient.AWS_EC2_METADATA_IPV4_ADDRESS} or ${AwsClient.AWS_EC2_METADATA_IPV6_ADDRESS}.` - ); - } - } - /** * Triggered when an external subject token is needed to be exchanged for a * GCP access token via GCP STS endpoint. diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 04e59866..58e9c8a1 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -229,59 +229,6 @@ describe('AwsClient', () => { assert.doesNotThrow(() => new AwsClient(validOptions)); }); - it('should throw when an unsupported credential_source is provided', () => { - const expectedError = new RangeError( - 'Invalid host "baddomain.com" for "url". Expecting 169.254.169.254 or fd00:ec2::254.' - ); - const invalidCredentialSource = Object.assign({}, awsCredentialSource); - invalidCredentialSource.url = 'http://baddomain.com/fake'; - const invalidOptions = { - type: 'external_account', - audience, - subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', - token_url: getTokenUrl(), - credential_source: invalidCredentialSource, - }; - - assert.throws(() => new AwsClient(invalidOptions), expectedError); - }); - - it('should throw when an unsupported imdsv2_session_token_url is provided', () => { - const expectedError = new RangeError( - 'Invalid host "baddomain.com" for "imdsv2_session_token_url". Expecting 169.254.169.254 or fd00:ec2::254.' - ); - const invalidCredentialSource = Object.assign( - {imdsv2_session_token_url: 'http://baddomain.com/fake'}, - awsCredentialSource - ); - const invalidOptions = { - type: 'external_account', - audience, - subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', - token_url: getTokenUrl(), - credential_source: invalidCredentialSource, - }; - - assert.throws(() => new AwsClient(invalidOptions), expectedError); - }); - - it('should throw when an unsupported region_url is provided', () => { - const expectedError = new RangeError( - 'Invalid host "baddomain.com" for "region_url". Expecting 169.254.169.254 or fd00:ec2::254.' - ); - const invalidCredentialSource = Object.assign({}, awsCredentialSource); - invalidCredentialSource.region_url = 'http://baddomain.com/fake'; - const invalidOptions = { - type: 'external_account', - audience, - subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', - token_url: getTokenUrl(), - credential_source: invalidCredentialSource, - }; - - assert.throws(() => new AwsClient(invalidOptions), expectedError); - }); - it('should throw when an unsupported environment ID is provided', () => { const expectedError = new Error( 'No valid AWS "credential_source" provided' From be252f599fa8b72cc7b63d013db43b24a5c336f9 Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Tue, 4 Apr 2023 13:59:51 -0700 Subject: [PATCH 414/662] build: add typings for additional query params for auth (#1534) * build: initial implementation of additional query params for auth --------- Co-authored-by: Daniel Bankhead --- src/auth/oauth2client.ts | 8 ++++++++ test/test.oauth2.ts | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 45bfe80f..a365277a 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -296,6 +296,14 @@ export interface GenerateAuthUrlOpts { * must be used with the 'code_challenge' parameter described above. */ code_challenge?: string; + + /** + * A way for developers and/or the auth team to provide a set of key value + * pairs to be added as query parameters to the authorization url. + */ + [ + key: string + ]: querystring.ParsedUrlQueryInput[keyof querystring.ParsedUrlQueryInput]; } export interface AccessTokenResponse { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 8152d011..a4946df9 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -74,6 +74,8 @@ describe('oauth2', () => { access_type: ACCESS_TYPE, scope: SCOPE, response_type: 'code token', + random: 'thing', + another: 'random_thing', }; const oauth2client = new OAuth2Client({ @@ -89,6 +91,8 @@ describe('oauth2', () => { assert.strictEqual(query.get('scope'), SCOPE); assert.strictEqual(query.get('client_id'), CLIENT_ID); assert.strictEqual(query.get('redirect_uri'), REDIRECT_URI); + assert.strictEqual(query.get('random'), 'thing'); + assert.strictEqual(query.get('another'), 'random_thing'); done(); }); From 86a4de82c0de3efeb4b9b05a6ef34bd98cce398c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 11 Apr 2023 14:49:10 +0100 Subject: [PATCH 415/662] fix(deps): update dependency @googleapis/iam to v6 (#1536) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 577f5037..ab6e45c2 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^6.0.0", - "@googleapis/iam": "^5.0.0", + "@googleapis/iam": "^6.0.0", "google-auth-library": "^8.7.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 06d4ef3cc9ba1651af5b1f90c02b231862822ba2 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Thu, 13 Apr 2023 14:27:45 -0700 Subject: [PATCH 416/662] feat: Add External Account Authorized User client type (#1530) * feature: adding external account authorized user client * fix: fix comment * addressing code review * cleaning up * Update src/auth/externalAccountAuthorizedUserClient.ts Co-authored-by: Daniel Bankhead * addressing review comments * lint --------- Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> Co-authored-by: Daniel Bankhead --- .../externalAccountAuthorizedUserClient.ts | 328 +++++++ src/auth/googleauth.ts | 11 + ...external-account-authorized-user-cred.json | 9 + ...est.externalaccountauthorizeduserclient.ts | 802 ++++++++++++++++++ test/test.googleauth.ts | 116 +++ 5 files changed, 1266 insertions(+) create mode 100644 src/auth/externalAccountAuthorizedUserClient.ts create mode 100644 test/fixtures/external-account-authorized-user-cred.json create mode 100644 test/test.externalaccountauthorizeduserclient.ts diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts new file mode 100644 index 00000000..c0c081cd --- /dev/null +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -0,0 +1,328 @@ +// Copyright 2023 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. + +import {AuthClient} from './authclient'; +import {Headers, RefreshOptions} from './oauth2client'; +import { + ClientAuthentication, + getErrorFromOAuthErrorResponse, + OAuthClientAuthHandler, + OAuthErrorResponse, +} from './oauth2common'; +import {BodyResponseCallback, DefaultTransporter} from '../transporters'; +import { + GaxiosError, + GaxiosOptions, + GaxiosPromise, + GaxiosResponse, +} from 'gaxios'; +import {Credentials} from './credentials'; +import * as stream from 'stream'; +import {EXPIRATION_TIME_OFFSET} from './baseexternalclient'; + +/** + * External Account Authorized User Credentials JSON interface. + */ +export interface ExternalAccountAuthorizedUserClientOptions { + type: typeof EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE; + audience: string; + client_id: string; + client_secret: string; + refresh_token: string; + token_url: string; + token_info_url: string; + revoke_url?: string; + quota_project_id?: string; +} + +/** + * The credentials JSON file type for external account authorized user clients. + */ +export const EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE = + 'external_account_authorized_user'; + +/** + * Internal interface for tracking the access token expiration time. + */ +interface CredentialsWithResponse extends Credentials { + res?: GaxiosResponse | null; +} + +/** + * Internal interface representing the token refresh response from the token_url endpoint. + */ +interface TokenRefreshResponse { + access_token: string; + expires_in: number; + refresh_token?: string; + res?: GaxiosResponse | null; +} + +/** + * Handler for token refresh requests sent to the token_url endpoint for external + * authorized user credentials. + */ +class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { + /** + * Initializes an ExternalAccountAuthorizedUserHandler instance. + * @param url The URL of the token refresh endpoint. + * @param transporter The transporter to use for the refresh request. + * @param clientAuthentication The client authentication credentials to use + * for the refresh request. + */ + constructor( + private readonly url: string, + private readonly transporter: DefaultTransporter, + clientAuthentication?: ClientAuthentication + ) { + super(clientAuthentication); + } + + /** + * Requests a new access token from the token_url endpoint using the provided + * refresh token. + * @param refreshToken The refresh token to use to generate a new access token. + * @param additionalHeaders Optional additional headers to pass along the + * request. + * @return A promise that resolves with the token refresh response containing + * the requested access token and its expiration time. + */ + async refreshToken( + refreshToken: string, + additionalHeaders?: Headers + ): Promise { + const values = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }); + + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + ...additionalHeaders, + }; + + const opts: GaxiosOptions = { + url: this.url, + method: 'POST', + headers, + data: values.toString(), + responseType: 'json', + }; + // Apply OAuth client authentication. + this.applyClientAuthenticationOptions(opts); + + try { + const response = await this.transporter.request( + opts + ); + // Successful response. + const tokenRefreshResponse = response.data; + tokenRefreshResponse.res = response; + return tokenRefreshResponse; + } catch (error) { + // Translate error to OAuthError. + if (error instanceof GaxiosError && error.response) { + throw getErrorFromOAuthErrorResponse( + error.response.data as OAuthErrorResponse, + // Preserve other fields from the original error. + error + ); + } + // Request could fail before the server responds. + throw error; + } + } +} + +/** + * External Account Authorized User Client. This is used for OAuth2 credentials + * sourced using external identities through Workforce Identity Federation. + * Obtaining the initial access and refresh token can be done through the + * Google Cloud CLI. + */ +export class ExternalAccountAuthorizedUserClient extends AuthClient { + private cachedAccessToken: CredentialsWithResponse | null; + private readonly externalAccountAuthorizedUserHandler: ExternalAccountAuthorizedUserHandler; + private refreshToken: string; + + /** + * Instantiates an ExternalAccountAuthorizedUserClient instances using the + * provided JSON object loaded from a credentials files. + * An error is throws if the credential is not valid. + * @param options The external account authorized user option object typically + * from the external accoutn authorized user JSON credential file. + * @param additionalOptions Optional additional behavior customization + * options. These currently customize expiration threshold time and + * whether to retry on 401/403 API request errors. + */ + constructor( + options: ExternalAccountAuthorizedUserClientOptions, + additionalOptions?: RefreshOptions + ) { + super(); + this.refreshToken = options.refresh_token; + const clientAuth = { + confidentialClientType: 'basic', + clientId: options.client_id, + clientSecret: options.client_secret, + } as ClientAuthentication; + this.externalAccountAuthorizedUserHandler = + new ExternalAccountAuthorizedUserHandler( + options.token_url, + this.transporter, + clientAuth + ); + + this.cachedAccessToken = null; + this.quotaProjectId = options.quota_project_id; + + // As threshold could be zero, + // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the + // zero value. + if (typeof additionalOptions?.eagerRefreshThresholdMillis !== 'number') { + this.eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET; + } else { + this.eagerRefreshThresholdMillis = additionalOptions! + .eagerRefreshThresholdMillis as number; + } + this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; + } + + async getAccessToken(): Promise<{ + token?: string | null; + res?: GaxiosResponse | null; + }> { + // If cached access token is unavailable or expired, force refresh. + if (!this.cachedAccessToken || this.isExpired(this.cachedAccessToken)) { + await this.refreshAccessTokenAsync(); + } + // Return GCP access token in GetAccessTokenResponse format. + return { + token: this.cachedAccessToken!.access_token, + res: this.cachedAccessToken!.res, + }; + } + + async getRequestHeaders(): Promise { + const accessTokenResponse = await this.getAccessToken(); + const headers: Headers = { + Authorization: `Bearer ${accessTokenResponse.token}`, + }; + return this.addSharedMetadataHeaders(headers); + } + + request(opts: GaxiosOptions): GaxiosPromise; + request(opts: GaxiosOptions, callback: BodyResponseCallback): void; + request( + opts: GaxiosOptions, + callback?: BodyResponseCallback + ): GaxiosPromise | void { + if (callback) { + this.requestAsync(opts).then( + r => callback(null, r), + e => { + return callback(e, e.response); + } + ); + } else { + return this.requestAsync(opts); + } + } + + /** + * Authenticates the provided HTTP request, processes it and resolves with the + * returned response. + * @param opts The HTTP request options. + * @param retry Whether the current attempt is a retry after a failed attempt. + * @return A promise that resolves with the successful response. + */ + protected async requestAsync( + opts: GaxiosOptions, + retry = false + ): Promise> { + let response: GaxiosResponse; + try { + const requestHeaders = await this.getRequestHeaders(); + opts.headers = opts.headers || {}; + if (requestHeaders && requestHeaders['x-goog-user-project']) { + opts.headers['x-goog-user-project'] = + requestHeaders['x-goog-user-project']; + } + if (requestHeaders && requestHeaders.Authorization) { + opts.headers.Authorization = requestHeaders.Authorization; + } + response = await this.transporter.request(opts); + } catch (e) { + const res = (e as GaxiosError).response; + if (res) { + const statusCode = res.status; + // Retry the request for metadata if the following criteria are true: + // - We haven't already retried. It only makes sense to retry once. + // - The response was a 401 or a 403 + // - The request didn't send a readableStream + // - forceRefreshOnFailure is true + const isReadableStream = res.config.data instanceof stream.Readable; + const isAuthErr = statusCode === 401 || statusCode === 403; + if ( + !retry && + isAuthErr && + !isReadableStream && + this.forceRefreshOnFailure + ) { + await this.refreshAccessTokenAsync(); + return await this.requestAsync(opts, true); + } + } + throw e; + } + return response; + } + + /** + * Forces token refresh, even if unexpired tokens are currently cached. + * @return A promise that resolves with the refreshed credential. + */ + protected async refreshAccessTokenAsync(): Promise { + // Refresh the access token using the refresh token. + const refreshResponse = + await this.externalAccountAuthorizedUserHandler.refreshToken( + this.refreshToken + ); + + this.cachedAccessToken = { + access_token: refreshResponse.access_token, + expiry_date: new Date().getTime() + refreshResponse.expires_in * 1000, + res: refreshResponse.res, + }; + + if (refreshResponse.refresh_token !== undefined) { + this.refreshToken = refreshResponse.refresh_token; + } + + return this.cachedAccessToken; + } + + /** + * Returns whether the provided credentials are expired or not. + * If there is no expiry time, assumes the token is not expired or expiring. + * @param credentials The credentials to check for expiration. + * @return Whether the credentials are expired or not. + */ + private isExpired(credentials: Credentials): boolean { + const now = new Date().getTime(); + return credentials.expiry_date + ? now >= credentials.expiry_date - this.eagerRefreshThresholdMillis + : false; + } +} diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 41f2a48e..77566cfb 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -48,6 +48,11 @@ import { BaseExternalAccountClient, } from './baseexternalclient'; import {AuthClient} from './authclient'; +import { + EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, + ExternalAccountAuthorizedUserClient, + ExternalAccountAuthorizedUserClientOptions, +} from './externalAccountAuthorizedUserClient'; /** * Defines all types of explicit clients that are determined via ADC JSON @@ -57,6 +62,7 @@ export type JSONClient = | JWT | UserRefreshClient | BaseExternalAccountClient + | ExternalAccountAuthorizedUserClient | Impersonated; export interface ProjectIdCallback { @@ -589,6 +595,11 @@ export class GoogleAuth { options )!; client.scopes = this.getAnyScopes(); + } else if (json.type === EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE) { + client = new ExternalAccountAuthorizedUserClient( + json as ExternalAccountAuthorizedUserClientOptions, + options + ); } else { (options as JWTOptions).scopes = this.scopes; client = new JWT(options); diff --git a/test/fixtures/external-account-authorized-user-cred.json b/test/fixtures/external-account-authorized-user-cred.json new file mode 100644 index 00000000..38273470 --- /dev/null +++ b/test/fixtures/external-account-authorized-user-cred.json @@ -0,0 +1,9 @@ +{ + "type": "external_account_authorized_user", + "audience": "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID", + "client_id": "clientId", + "client_secret": "clientSecret", + "refresh_token": "refreshToken", + "token_url": "https://sts.googleapis.com/v1/oauthtoken", + "token_info_url": "https://sts.googleapis.com/v1/introspect" +} diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts new file mode 100644 index 00000000..879f48e9 --- /dev/null +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -0,0 +1,802 @@ +// Copyright 2023 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. + +import * as assert from 'assert'; +import {describe, it, afterEach, beforeEach} from 'mocha'; +import * as nock from 'nock'; +import * as sinon from 'sinon'; +import * as qs from 'querystring'; +import {assertGaxiosResponsePresent, getAudience} from './externalclienthelper'; +import { + EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, + ExternalAccountAuthorizedUserClient, + ExternalAccountAuthorizedUserClientOptions, +} from '../src/auth/externalAccountAuthorizedUserClient'; +import {EXPIRATION_TIME_OFFSET} from '../src/auth/baseexternalclient'; +import {GaxiosError, GaxiosResponse} from 'gaxios'; +import { + getErrorFromOAuthErrorResponse, + OAuthErrorResponse, +} from '../src/auth/oauth2common'; + +nock.disableNetConnect(); + +describe('ExternalAccountAuthorizedUserClient', () => { + const BASE_URL = 'https://sts.googleapis.com'; + const REFRESH_PATH = '/v1/oauthtoken'; + const TOKEN_REFRESH_URL = `${BASE_URL}${REFRESH_PATH}`; + const TOKEN_INFO_URL = `${BASE_URL}/v1/introspect`; + + interface TokenRefreshResponse { + access_token: string; + expires_in: number; + refresh_token?: string; + res?: GaxiosResponse | null; + } + + interface NockMockRefreshResponse { + statusCode: number; + response: TokenRefreshResponse | OAuthErrorResponse; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: {[key: string]: any}; + times?: number; + additionalHeaders?: {[key: string]: string}; + } + + function mockStsTokenRefresh( + url: string, + path: string, + nockParams: NockMockRefreshResponse[] + ): nock.Scope { + const scope = nock(url); + nockParams.forEach(nockMockStsToken => { + const times = + nockMockStsToken.times !== undefined ? nockMockStsToken.times : 1; + const headers = Object.assign( + { + 'content-type': 'application/x-www-form-urlencoded', + }, + nockMockStsToken.additionalHeaders || {} + ); + scope + .post(path, qs.stringify(nockMockStsToken.request), { + reqheaders: headers, + }) + .times(times) + .reply(nockMockStsToken.statusCode, nockMockStsToken.response); + }); + return scope; + } + + let clock: sinon.SinonFakeTimers; + const referenceDate = new Date('2020-08-11T06:55:22.345Z'); + const audience = getAudience(); + const externalAccountAuthorizedUserCredentialOptions = { + type: EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, + audience: audience, + client_id: 'clientId', + client_secret: 'clientSecret', + refresh_token: 'refreshToken', + token_url: TOKEN_REFRESH_URL, + token_info_url: TOKEN_INFO_URL, + } as ExternalAccountAuthorizedUserClientOptions; + const successfulRefreshResponse = { + access_token: 'newAccessToken', + refresh_token: 'newRefreshToken', + expires_in: 3600, + }; + const successfulRefreshResponseNoRefreshToken = { + access_token: 'newAccessToken', + expires_in: 3600, + }; + beforeEach(() => { + clock = sinon.useFakeTimers(referenceDate); + }); + + afterEach(() => { + if (clock) { + clock.restore(); + } + }); + + describe('Constructor', () => { + it('should not throw when valid options are provided', () => { + assert.doesNotThrow(() => { + return new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + }); + }); + + it('should set default RefreshOptions', () => { + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + + assert(!client.forceRefreshOnFailure); + assert(client.eagerRefreshThresholdMillis === EXPIRATION_TIME_OFFSET); + }); + + it('should set custom RefreshOptions', () => { + const refreshOptions = { + eagerRefreshThresholdMillis: 5000, + forceRefreshOnFailure: true, + }; + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions, + refreshOptions + ); + + assert.strictEqual( + client.forceRefreshOnFailure, + refreshOptions.forceRefreshOnFailure + ); + assert.strictEqual( + client.eagerRefreshThresholdMillis, + refreshOptions.eagerRefreshThresholdMillis + ); + }); + }); + + describe('getAccessToken()', () => { + it('should resolve with the expected response', async () => { + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponse, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + const actualResponse = await client.getAccessToken(); + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: successfulRefreshResponse.access_token, + }); + scope.done(); + }); + + it('should handle refresh errors', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid refresh token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + await assert.rejects( + client.getAccessToken(), + getErrorFromOAuthErrorResponse(errorResponse) + ); + scope.done(); + }); + + it('should handle refresh timeout', async () => { + const expectedRequest = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }); + + const scope = nock(BASE_URL) + .post(REFRESH_PATH, expectedRequest.toString(), { + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }) + .replyWithError({code: 'ETIMEDOUT'}); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + await assert.rejects(client.getAccessToken(), { + code: 'ETIMEDOUT', + }); + scope.done(); + }); + + it('should use the new refresh token', async () => { + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponse, + request: { + grant_type: 'refresh_token', + refresh_token: + externalAccountAuthorizedUserCredentialOptions.refresh_token, + }, + }, + { + statusCode: 200, + response: successfulRefreshResponse, + request: { + grant_type: 'refresh_token', + refresh_token: successfulRefreshResponse.refresh_token, + }, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + // Get initial access token and new refresh token. + await client.getAccessToken(); + // Advance clock to force new refresh. + clock.tick((successfulRefreshResponse.expires_in + 1) * 1000); + // Refresh access token with new access token. + const actualResponse = await client.getAccessToken(); + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: successfulRefreshResponse.access_token, + }); + + scope.done(); + }); + + it('should not call refresh when token is cached', async () => { + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + // Get initial access token and new refresh token. + await client.getAccessToken(); + // Advance clock to force new refresh. + clock.tick( + successfulRefreshResponseNoRefreshToken.expires_in * 1000 - + client.eagerRefreshThresholdMillis - + 1 + ); + // Refresh access token with new access token. + const actualResponse = await client.getAccessToken(); + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: successfulRefreshResponse.access_token, + }); + + scope.done(); + }); + + it('should refresh when cached token is expired', async () => { + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + times: 2, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + // Get initial access token. + await client.getAccessToken(); + // Advance clock to force new refresh. + clock.tick( + successfulRefreshResponseNoRefreshToken.expires_in * 1000 - + client.eagerRefreshThresholdMillis + + 1 + ); + // Refresh access token with new access token. + const actualResponse = await client.getAccessToken(); + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: successfulRefreshResponse.access_token, + }); + + scope.done(); + }); + }); + + describe('getRequestHeaders()', () => { + it('should inject the authorization headers', async () => { + const expectedHeaders = { + Authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, + 'x-goog-user-project': 'quotaProjectId', + }; + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + + const optionsWithQuotaProjectId = Object.assign( + {quota_project_id: 'quotaProjectId'}, + externalAccountAuthorizedUserCredentialOptions + ); + const client = new ExternalAccountAuthorizedUserClient( + optionsWithQuotaProjectId + ); + const actualHeaders = await client.getRequestHeaders(); + + assert.deepStrictEqual(actualHeaders, expectedHeaders); + scope.done(); + }); + + it('should reject when error occurs during token retrieval', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + await assert.rejects( + client.getRequestHeaders(), + getErrorFromOAuthErrorResponse(errorResponse) + ); + scope.done(); + }); + }); + + describe('request()', () => { + it('should process HTTP request with authorization header', async () => { + const quotaProjectId = 'QUOTA_PROJECT_ID'; + const authHeaders = { + Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + 'x-goog-user-project': quotaProjectId, + }; + const optionsWithQuotaProjectId = Object.assign( + {quota_project_id: quotaProjectId}, + externalAccountAuthorizedUserCredentialOptions + ); + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new ExternalAccountAuthorizedUserClient( + optionsWithQuotaProjectId + ); + const actualResponse = await client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should reject when error occurs during token retrieval', async () => { + const errorResponse: OAuthErrorResponse = { + error: 'invalid_request', + error_description: 'Invalid subject token', + error_uri: 'https://tools.ietf.org/html/rfc6749#section-5.2', + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 400, + response: errorResponse, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + await assert.rejects( + client.request({ + url: 'https://example.com/api', + method: 'POST', + data: exampleRequest, + responseType: 'json', + }), + getErrorFromOAuthErrorResponse(errorResponse) + ); + scope.done(); + }); + + it('should trigger callback on success when provided', done => { + const authHeaders = { + Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + client.request( + { + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }, + (err, result) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(result?.data, exampleResponse); + scopes.forEach(scope => scope.done()); + done(); + } + ); + }); + + it('should trigger callback on error when provided', done => { + const errorMessage = 'Bad Request'; + const authHeaders = { + Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(400, errorMessage), + ]; + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + client.request( + { + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }, + (err, result) => { + assert.strictEqual(err!.message, errorMessage); + assert.deepStrictEqual(result, (err as GaxiosError)!.response); + scopes.forEach(scope => scope.done()); + done(); + } + ); + }); + + it('should retry on 401 on forceRefreshOnFailure=true', async () => { + const authHeaders = { + Authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse = { + foo: 'a', + bar: 1, + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + times: 2, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(401) + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions, + { + forceRefreshOnFailure: true, + } + ); + const actualResponse = await client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + + it('should not retry on 401 on forceRefreshOnFailure=false', async () => { + const authHeaders = { + Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponse, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(401), + ]; + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions, + { + forceRefreshOnFailure: false, + } + ); + await assert.rejects( + client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }), + { + code: '401', + } + ); + + scopes.forEach(scope => scope.done()); + }); + + it('should not retry more than once', async () => { + const authHeaders = { + Authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleHeaders = { + custom: 'some-header-value', + other: 'other-header-value', + }; + const scopes = [ + mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + times: 2, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(403) + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .reply(403), + ]; + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions, + { + forceRefreshOnFailure: true, + } + ); + await assert.rejects( + client.request({ + url: 'https://example.com/api', + method: 'POST', + headers: exampleHeaders, + data: exampleRequest, + responseType: 'json', + }), + { + code: '403', + } + ); + scopes.forEach(scope => scope.done()); + }); + + it('should process headerless HTTP request', async () => { + const authHeaders = { + Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + }; + const exampleRequest = { + key1: 'value1', + key2: 'value2', + }; + const exampleResponse = { + foo: 'a', + bar: 1, + }; + const scopes = [ + mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponseNoRefreshToken, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]), + nock('https://example.com') + .post('/api', exampleRequest, { + reqheaders: Object.assign({}, authHeaders), + }) + .reply(200, Object.assign({}, exampleResponse)), + ]; + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + // Send request with no headers. + const actualResponse = await client.request({ + url: 'https://example.com/api', + method: 'POST', + data: exampleRequest, + responseType: 'json', + }); + + assert.deepStrictEqual(actualResponse.data, exampleResponse); + scopes.forEach(scope => scope.done()); + }); + }); +}); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index e5c99424..04be4314 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -53,6 +53,10 @@ import { } from './externalclienthelper'; import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; import {AuthClient} from '../src/auth/authclient'; +import { + ExternalAccountAuthorizedUserClient, + ExternalAccountAuthorizedUserClientOptions, +} from '../src/auth/externalAccountAuthorizedUserClient'; nock.disableNetConnect(); @@ -82,6 +86,8 @@ describe('googleauth', () => { const refreshJSON = require('../../test/fixtures/refresh.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires const externalAccountJSON = require('../../test/fixtures/external-account-cred.json'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const externalAccountAuthorizedUserJSON = require('../../test/fixtures/external-account-authorized-user-cred.json'); const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); const wellKnownPathWindows = path.join( 'C:', @@ -2457,6 +2463,116 @@ describe('googleauth', () => { }); }); }); + + describe('for external_account_authorized_user types', () => { + /** + * @return A copy of the external account authorized user JSON auth object + * for testing. + */ + function createExternalAccountAuthorizedUserJson() { + return Object.assign({}, externalAccountAuthorizedUserJSON); + } + + describe('fromJSON()', () => { + it('should create the expected BaseExternalAccountClient', () => { + const json = createExternalAccountAuthorizedUserJson(); + const result = auth.fromJSON(json); + assert(result instanceof ExternalAccountAuthorizedUserClient); + }); + }); + + describe('fromStream()', () => { + it('should read the stream and create a client', async () => { + const stream = fs.createReadStream( + './test/fixtures/external-account-authorized-user-cred.json' + ); + const actualClient = await auth.fromStream(stream); + + assert(actualClient instanceof ExternalAccountAuthorizedUserClient); + }); + }); + + describe('getApplicationDefault()', () => { + it('should use environment variable when it is set', async () => { + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-authorized-user-cred.json' + ); + + const res = await auth.getApplicationDefault(); + const actualClient = res.credential; + + assert(actualClient instanceof ExternalAccountAuthorizedUserClient); + }); + + it('should use well-known file when it is available and env const is not set', async () => { + mockLinuxWellKnownFile( + './test/fixtures/external-account-authorized-user-cred.json' + ); + + const res = await auth.getApplicationDefault(); + const actualClient = res.credential; + + assert(actualClient instanceof ExternalAccountAuthorizedUserClient); + }); + }); + + describe('getApplicationCredentialsFromFilePath()', () => { + it('should correctly read the file and create a valid client', async () => { + const actualClient = + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/external-account-authorized-user-cred.json' + ); + + assert(actualClient instanceof ExternalAccountAuthorizedUserClient); + }); + }); + + describe('getClient()', () => { + it('should initialize from credentials', async () => { + const auth = new GoogleAuth({ + credentials: createExternalAccountAuthorizedUserJson(), + }); + const actualClient = await auth.getClient(); + + assert(actualClient instanceof ExternalAccountAuthorizedUserClient); + }); + + it('should initialize from keyFileName', async () => { + const keyFilename = + './test/fixtures/external-account-authorized-user-cred.json'; + const auth = new GoogleAuth({keyFilename}); + const actualClient = await auth.getClient(); + + assert(actualClient instanceof ExternalAccountAuthorizedUserClient); + }); + + it('should initialize from ADC', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-authorized-user-cred.json' + ); + const auth = new GoogleAuth(); + const actualClient = await auth.getClient(); + + assert(actualClient instanceof ExternalAccountAuthorizedUserClient); + }); + }); + + describe('sign()', () => { + it('should reject', async () => { + const auth = new GoogleAuth({ + credentials: createExternalAccountAuthorizedUserJson(), + }); + + await assert.rejects( + auth.sign('abc123'), + /Cannot sign data without `client_email`/ + ); + }); + }); + }); }); // Allows a client to be instantiated from a certificate, From fdb67dc634f7fa2fcf8977d6d488cc91e9a7cccb Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 19 Apr 2023 20:42:24 +0200 Subject: [PATCH 417/662] fix(deps): update dependency @googleapis/iam to v7 (#1538) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index ab6e45c2..c68c3642 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^6.0.0", - "@googleapis/iam": "^6.0.0", + "@googleapis/iam": "^7.0.0", "google-auth-library": "^8.7.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", From c6138be12753c68d7645843451e1e3f9146e515a Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:24:15 -0700 Subject: [PATCH 418/662] feat: Document External Account Authorized User Credentials (#1540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adding documentation for external account authorized credentials * fix: use readme-partials * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: code review comments * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 23 +++++++++++++++++++++++ README.md | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 3284dc89..21334d0a 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -477,6 +477,29 @@ body: |- - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. + #### Using External Account Authorized User workforce credentials + + [External account authorized user credentials](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#browser-based-sign-in) allow you to sign in with a web browser to an external identity provider account via the + gcloud CLI and create a configuration for the auth library to use. + + To generate an external account authorized user workforce identity configuration, run the following command: + + ```bash + gcloud auth application-default login --login-config=$LOGIN_CONFIG + ``` + + Where the following variable needs to be substituted: + - `$LOGIN_CONFIG`: The login config file generated with the cloud console or + [gcloud iam workforce-pools create-login-config](https://cloud.google.com/sdk/gcloud/reference/iam/workforce-pools/create-login-config) + + This will open a browser flow for you to sign in via the configured third party identity provider + and then will store the external account authorized user configuration at the well known ADC location. + The auth library will then use the provided refresh token from the configuration to generate and refresh + an access token to call Google Cloud services. + + Note that the default lifetime of the refresh token is one hour, after which a new configuration will need to be generated from the gcloud CLI. + The lifetime can be modified by changing the [session duration of the workforce pool](https://cloud.google.com/iam/docs/reference/rest/v1/locations.workforcePools), and can be set as high as 12 hours. + #### Using Executable-sourced credentials with OIDC and SAML **Executable-sourced credentials** diff --git a/README.md b/README.md index 13539195..0fef4af0 100644 --- a/README.md +++ b/README.md @@ -521,6 +521,29 @@ Where the following variables need to be substituted: - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. +#### Using External Account Authorized User workforce credentials + +[External account authorized user credentials](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#browser-based-sign-in) allow you to sign in with a web browser to an external identity provider account via the +gcloud CLI and create a configuration for the auth library to use. + +To generate an external account authorized user workforce identity configuration, run the following command: + +```bash +gcloud auth application-default login --login-config=$LOGIN_CONFIG +``` + +Where the following variable needs to be substituted: +- `$LOGIN_CONFIG`: The login config file generated with the cloud console or + [gcloud iam workforce-pools create-login-config](https://cloud.google.com/sdk/gcloud/reference/iam/workforce-pools/create-login-config) + +This will open a browser flow for you to sign in via the configured third party identity provider +and then will store the external account authorized user configuration at the well known ADC location. +The auth library will then use the provided refresh token from the configuration to generate and refresh +an access token to call Google Cloud services. + +Note that the default lifetime of the refresh token is one hour, after which a new configuration will need to be generated from the gcloud CLI. +The lifetime can be modified by changing the [session duration of the workforce pool](https://cloud.google.com/iam/docs/reference/rest/v1/locations.workforcePools), and can be set as high as 12 hours. + #### Using Executable-sourced credentials with OIDC and SAML **Executable-sourced credentials** From 85f2c9393f64b97290a0698d3ac6754fa9bc951a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 19:59:50 -0700 Subject: [PATCH 419/662] chore(main): release 8.8.0 (#1496) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c64c58..da482049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [8.8.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.7.0...v8.8.0) (2023-04-25) + + +### Features + +* Add External Account Authorized User client type ([#1530](https://github.com/googleapis/google-auth-library-nodejs/issues/1530)) ([06d4ef3](https://github.com/googleapis/google-auth-library-nodejs/commit/06d4ef3cc9ba1651af5b1f90c02b231862822ba2)) +* Document External Account Authorized User Credentials ([#1540](https://github.com/googleapis/google-auth-library-nodejs/issues/1540)) ([c6138be](https://github.com/googleapis/google-auth-library-nodejs/commit/c6138be12753c68d7645843451e1e3f9146e515a)) + + +### Bug Fixes + +* **deps:** Update `gcp-metadata` to v5.2.0 ([#1502](https://github.com/googleapis/google-auth-library-nodejs/issues/1502)) ([2562d11](https://github.com/googleapis/google-auth-library-nodejs/commit/2562d1192e0f89a3232897b8e27f24a14d5222f2)) +* **deps:** Update dependency @googleapis/iam to v4 ([#1482](https://github.com/googleapis/google-auth-library-nodejs/issues/1482)) ([00d6135](https://github.com/googleapis/google-auth-library-nodejs/commit/00d6135f35a1aa193d50fad6b3ec28a7fda9df66)) +* **deps:** Update dependency @googleapis/iam to v5 ([#1526](https://github.com/googleapis/google-auth-library-nodejs/issues/1526)) ([a1f9835](https://github.com/googleapis/google-auth-library-nodejs/commit/a1f9835fe155722206046d6bb5b56f9e53f2fe9a)) +* **deps:** Update dependency @googleapis/iam to v6 ([#1536](https://github.com/googleapis/google-auth-library-nodejs/issues/1536)) ([86a4de8](https://github.com/googleapis/google-auth-library-nodejs/commit/86a4de82c0de3efeb4b9b05a6ef34bd98cce398c)) +* **deps:** Update dependency @googleapis/iam to v7 ([#1538](https://github.com/googleapis/google-auth-library-nodejs/issues/1538)) ([fdb67dc](https://github.com/googleapis/google-auth-library-nodejs/commit/fdb67dc634f7fa2fcf8977d6d488cc91e9a7cccb)) +* Do not call metadata server if security creds and region are retrievable through environment vars ([#1493](https://github.com/googleapis/google-auth-library-nodejs/issues/1493)) ([d4de941](https://github.com/googleapis/google-auth-library-nodejs/commit/d4de9412e12f1f6f23f2a7c0d176dc5d2543e607)) +* Removing 3pi config URL validation ([#1517](https://github.com/googleapis/google-auth-library-nodejs/issues/1517)) ([a278d19](https://github.com/googleapis/google-auth-library-nodejs/commit/a278d19a0b211b13e5cf5176d40128e704b55780)) +* Removing aws url validation ([#1531](https://github.com/googleapis/google-auth-library-nodejs/issues/1531)) ([f4d9335](https://github.com/googleapis/google-auth-library-nodejs/commit/f4d933579cb5b9e50adf6e679a73cc78388ad8f8)) + ## [8.7.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.6.0...v8.7.0) (2022-11-08) diff --git a/package.json b/package.json index 096d13a5..855d4fe5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.7.0", + "version": "8.8.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index c68c3642..cd617b67 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^7.0.0", - "google-auth-library": "^8.7.0", + "google-auth-library": "^8.8.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 7412d7c55c688601199b5201b29cfbe63c4c7a70 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Wed, 17 May 2023 14:21:47 -0700 Subject: [PATCH 420/662] feat: adds universe_domain field to base external client (#1548) --- src/auth/baseexternalclient.ts | 4 +++- test/test.baseexternalclient.ts | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index ddd79d8e..81540014 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -76,6 +76,7 @@ export interface BaseExternalAccountClientOptions { client_secret?: string; quota_project_id?: string; workforce_pool_user_project?: string; + universe_domain?: string; } /** @@ -137,9 +138,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { private readonly workforcePoolUserProject?: string; public projectId: string | null; public projectNumber: string | null; + public universeDomain?: string; public readonly eagerRefreshThresholdMillis: number; public readonly forceRefreshOnFailure: boolean; - /** * Instantiate a BaseExternalAccountClient instance using the provided JSON * object loaded from an external account credentials file. @@ -205,6 +206,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; this.projectId = null; this.projectNumber = this.getProjectNumber(this.audience); + this.universeDomain = options.universe_domain; } /** The service account email to be impersonated, if available. */ diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 6c98df7d..0b89e840 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -283,6 +283,26 @@ describe('BaseExternalAccountClient', () => { }); }); + describe('universeDomain', () => { + it('should be undefined if not set', () => { + const client = new TestExternalAccountClient(externalAccountOptions); + + assert(client.universeDomain === undefined); + }); + + it('should be set if provided', () => { + const universeDomain = 'universe.domain.com'; + const options: BaseExternalAccountClientOptions = Object.assign( + {}, + externalAccountOptions + ); + options.universe_domain = universeDomain; + const client = new TestExternalAccountClient(options); + + assert.equal(client.universeDomain, universeDomain); + }); + }); + describe('getServiceAccountEmail()', () => { it('should return the service account email when impersonation is used', () => { const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; From 73b63d56fad8da033307e1be501a8846a5311211 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Fri, 19 May 2023 13:55:54 -0700 Subject: [PATCH 421/662] fix: make universeDomain private (#1552) * feat: adds universe_domain field to base external client * fix: make universeDomain private * fixing merge issue --- src/auth/baseexternalclient.ts | 2 +- test/test.baseexternalclient.ts | 21 +-------------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 81540014..1aab7123 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -136,9 +136,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { private readonly stsCredential: sts.StsCredentials; private readonly clientAuth?: ClientAuthentication; private readonly workforcePoolUserProject?: string; + private universeDomain?: string; public projectId: string | null; public projectNumber: string | null; - public universeDomain?: string; public readonly eagerRefreshThresholdMillis: number; public readonly forceRefreshOnFailure: boolean; /** diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 0b89e840..917ff6b4 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -70,6 +70,7 @@ describe('BaseExternalAccountClient', () => { credential_source: { file: '/var/run/secrets/goog.id/token', }, + universe_domain: 'universe.domain.com', }; const externalAccountOptionsWithCreds = { type: 'external_account', @@ -283,26 +284,6 @@ describe('BaseExternalAccountClient', () => { }); }); - describe('universeDomain', () => { - it('should be undefined if not set', () => { - const client = new TestExternalAccountClient(externalAccountOptions); - - assert(client.universeDomain === undefined); - }); - - it('should be set if provided', () => { - const universeDomain = 'universe.domain.com'; - const options: BaseExternalAccountClientOptions = Object.assign( - {}, - externalAccountOptions - ); - options.universe_domain = universeDomain; - const client = new TestExternalAccountClient(options); - - assert.equal(client.universeDomain, universeDomain); - }); - }); - describe('getServiceAccountEmail()', () => { it('should return the service account email when impersonation is used', () => { const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; From 3563c16e2376729e399fefc0b1fad7bff78c296e Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 23 May 2023 15:50:16 +0200 Subject: [PATCH 422/662] fix(deps): update dependency puppeteer to v20 (#1550) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 855d4fe5..ab21527a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^18.0.0", + "puppeteer": "^20.0.0", "sinon": "^15.0.0", "ts-loader": "^8.0.0", "typescript": "^4.6.3", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 14c00a7e..565b8d35 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^8.0.0", - "puppeteer": "^18.0.0" + "puppeteer": "^20.0.0" } } From ce6013c6132ba2b82a1f8cba0a15c4710536204c Mon Sep 17 00:00:00 2001 From: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Date: Wed, 31 May 2023 14:24:39 -0700 Subject: [PATCH 423/662] fix: remove auth lib version from x-goog-api-client header (#1558) --- src/transporters.ts | 14 ++------------ test/test.transporters.ts | 14 +++----------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/transporters.ts b/src/transporters.ts index 33dca5ff..0b4e2617 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -68,19 +68,9 @@ export class DefaultTransporter implements Transporter { ] = `${uaValue} ${DefaultTransporter.USER_AGENT}`; } // track google-auth-library-nodejs version: - const authVersion = `auth/${pkg.version}`; - if ( - opts.headers['x-goog-api-client'] && - !opts.headers['x-goog-api-client'].includes(authVersion) - ) { - opts.headers[ - 'x-goog-api-client' - ] = `${opts.headers['x-goog-api-client']} ${authVersion}`; - } else if (!opts.headers['x-goog-api-client']) { + if (!opts.headers['x-goog-api-client']) { const nodeVersion = process.version.replace(/^v/, ''); - opts.headers[ - 'x-goog-api-client' - ] = `gl-node/${nodeVersion} ${authVersion}`; + opts.headers['x-goog-api-client'] = `gl-node/${nodeVersion}`; } } return opts; diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 08410db4..97a72465 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -58,11 +58,7 @@ describe('transporters', () => { const opts = transporter.configure({ url: '', }); - assert( - /^gl-node\/[.-\w$]+ auth\/[.-\w$]+$/.test( - opts.headers!['x-goog-api-client'] - ) - ); + assert(/^gl-node\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client'])); }); it('should append to x-goog-api-client header if it exists', () => { @@ -70,9 +66,7 @@ describe('transporters', () => { headers: {'x-goog-api-client': 'gdcl/1.0.0'}, url: '', }); - assert( - /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client']) - ); + assert(/^gdcl\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client'])); }); // see: https://github.com/googleapis/google-auth-library-nodejs/issues/819 @@ -84,9 +78,7 @@ describe('transporters', () => { let configuredOpts = transporter.configure(opts); configuredOpts = transporter.configure(opts); assert( - /^gdcl\/[.-\w$]+ auth\/[.-\w$]+$/.test( - configuredOpts.headers!['x-goog-api-client'] - ) + /^gdcl\/[.-\w$]+$/.test(configuredOpts.headers!['x-goog-api-client']) ); }); From e3ab8247761b0a61f25e08a1b8fd6b200a4be200 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Sat, 3 Jun 2023 08:36:07 -0700 Subject: [PATCH 424/662] fix: keepAlive sample (#1561) * fix: keep alive sample The property should be `agent` * chore: copyright header --- samples/keepalive.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/samples/keepalive.js b/samples/keepalive.js index 9102be2f..2cab0e19 100644 --- a/samples/keepalive.js +++ b/samples/keepalive.js @@ -1,4 +1,4 @@ -// Copyright 2017, Google, Inc. +// Copyright 2023 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 @@ -41,17 +41,11 @@ async function main() { const agent = new https.Agent({keepAlive: true}); // use the agent as an Axios config param to make the request - const res = await client.request({ - url, - httpsAgent: agent, - }); + const res = await client.request({url, agent}); console.log(res.data); // Re-use the same agent to make the next request over the same connection - const res2 = await client.request({ - url, - httpsAgent: agent, - }); + const res2 = await client.request({url, agent}); console.log(res2.data); } From efcdef190fd94537d79087e7fcb26e5925a46173 Mon Sep 17 00:00:00 2001 From: Ayush Walekar Date: Wed, 7 Jun 2023 02:40:24 +0530 Subject: [PATCH 425/662] fix: IdTokenClient expiry_date check (#1555) to respect eagerRefreshThresholdMillis of parent class `OAuth2Client` Co-authored-by: Daniel Bankhead --- src/auth/idtokenclient.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/auth/idtokenclient.ts b/src/auth/idtokenclient.ts index e123be29..38a72278 100644 --- a/src/auth/idtokenclient.ts +++ b/src/auth/idtokenclient.ts @@ -52,7 +52,8 @@ export class IdTokenClient extends OAuth2Client { ): Promise { if ( !this.credentials.id_token || - (this.credentials.expiry_date || 0) < Date.now() + !this.credentials.expiry_date || + this.isTokenExpiring() ) { const idToken = await this.idTokenProvider.fetchIdToken( this.targetAudience From 070ec96b78dc26791bacb452ebef13d0a5ae6b18 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 21:24:15 +0000 Subject: [PATCH 426/662] docs: update docs-devsite.sh to use latest node-js-rad version (#1553) Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> Source-Link: https://togithub.com/googleapis/synthtool/commit/b1ced7db5adee08cfa91d6b138679fceff32c004 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:0527a86c10b67742c409dc726ba9a31ec4e69b0006e3d7a49b0e6686c59cdaa9 --- .github/.OwlBot.lock.yaml | 3 ++- .kokoro/release/docs-devsite.sh | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 0b836e11..21ad18bd 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,4 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:e6d785d6de3cab027f6213d95ccedab4cab3811b0d3172b78db2216faa182e32 + digest: sha256:0527a86c10b67742c409dc726ba9a31ec4e69b0006e3d7a49b0e6686c59cdaa9 +# created: 2023-05-24T20:32:43.844586914Z diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index 2198e67f..3596c1e4 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -25,5 +25,6 @@ if [[ -z "$CREDENTIALS" ]]; then fi npm install -npm install --no-save @google-cloud/cloud-rad@^0.2.5 -npx @google-cloud/cloud-rad \ No newline at end of file +npm install --no-save @google-cloud/cloud-rad@^0.3.7 +# publish docs to devsite +npx @google-cloud/cloud-rad . cloud-rad From fe840490ac2fa593e59968c07464eaf538613818 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 28 Jun 2023 21:31:41 -0700 Subject: [PATCH 427/662] refactor: increase upper webpack bundle limit (#1580) --- system-test/test.kitchen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 3111533b..8e1e89f6 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -64,7 +64,7 @@ describe('pack and install', () => { await execa('npx', ['webpack'], {cwd: `${stagingDir}/`, stdio: 'inherit'}); const bundle = path.join(stagingDir, 'dist', 'bundle.min.js'); const stat = fs.statSync(bundle); - assert(stat.size < 256 * 1024); + assert(stat.size < 512 * 1024); }); /** From 5e6d6a20ef1cab3fd12173b98d3e7f7bac6da727 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 29 Jun 2023 06:37:49 +0200 Subject: [PATCH 428/662] chore(deps): update dependency c8 to v8 (#1564) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab21527a..88c701f0 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/node": "^16.0.0", "@types/sinon": "^10.0.0", "assert-rejects": "^1.0.0", - "c8": "^7.0.0", + "c8": "^8.0.0", "chai": "^4.2.0", "codecov": "^3.0.2", "execa": "^5.0.0", From 43377ac7af1ece14b82b053b504449f257d11af6 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 29 Jun 2023 07:20:27 -0700 Subject: [PATCH 429/662] feat: Utilize `gcp-metadata`'s GCP Residency Check (#1513) * feat: utilize `gcp-residency` check Removes 1 network check for workloads running on GCP * test: Update tests for gcp residency detection * refactor: streamline logic and update documentation * chore: stash * chore: Update `gcp-metadata` * refactor: use `getGCPResidency` * test: Dedup gcp-metadata tests * fix: await --- package.json | 2 +- src/auth/googleauth.ts | 7 ++++- test/test.googleauth.ts | 64 +++++------------------------------------ 3 files changed, 14 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 88c701f0..bf2331bd 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^5.0.0", - "gcp-metadata": "^5.2.0", + "gcp-metadata": "^5.3.0", "gtoken": "^6.1.0", "jws": "^4.0.0", "lru-cache": "^6.0.0" diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 77566cfb..ca31fb13 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -396,13 +396,18 @@ export class GoogleAuth { /** * Determines whether the auth layer is running on Google Compute Engine. + * Checks for GCP Residency, then fallback to checking if metadata server + * is available. + * * @returns A promise that resolves with the boolean. * @api private */ async _checkIsGCE() { if (this.checkIsGCE === undefined) { - this.checkIsGCE = await gcpMetadata.isAvailable(); + this.checkIsGCE = + gcpMetadata.getGCPResidency() || (await gcpMetadata.isAvailable()); } + return this.checkIsGCE; } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 04be4314..eca30109 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -25,6 +25,7 @@ import { SECONDARY_HOST_ADDRESS, resetIsAvailableCache, } from 'gcp-metadata'; +import * as gcpMetadata from 'gcp-metadata'; import * as nock from 'nock'; import * as os from 'os'; import * as path from 'path'; @@ -1137,66 +1138,15 @@ describe('googleauth', () => { assert.strictEqual(undefined, client.scope); }); - it('_checkIsGCE should set the _isGCE flag when running on GCE', async () => { - assert.notStrictEqual(true, auth.isGCE); - const scope = nockIsGCE(); - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - scope.done(); - }); - - it('_checkIsGCE should not set the _isGCE flag when not running on GCE', async () => { - const scope = nockNotGCE(); - assert.notStrictEqual(true, auth.isGCE); - await auth._checkIsGCE(); - assert.strictEqual(false as boolean, auth.isGCE); - scope.done(); - }); - - it('_checkIsGCE should retry the check for isGCE on transient http errors', async () => { - assert.notStrictEqual(true, auth.isGCE); - // the first request will fail, the second one will succeed - const scopes = [nock500GCE(), nockIsGCE()]; - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - scopes.forEach(s => s.done()); - }); - - it('_checkIsGCE should return false on unexpected errors', async () => { - assert.notStrictEqual(true, auth.isGCE); - const scope = nock500GCE(); - assert.strictEqual(await auth._checkIsGCE(), false); - assert.strictEqual(auth.isGCE, false); - scope.done(); - }); + it("_checkIsGCE should be equalivalent should use GCP metadata's checks", async () => { + nockNotGCE(); - it('_checkIsGCE should not retry the check for isGCE if it fails with an ENOTFOUND', async () => { - assert.notStrictEqual(true, auth.isGCE); - const scope = nockNotGCE(); - await auth._checkIsGCE(); - assert.strictEqual(false as boolean, auth.isGCE); - scope.done(); - }); - - it('_checkIsGCE does not execute the second time when running on GCE', async () => { - // This test relies on the nock mock only getting called once. - assert.notStrictEqual(true, auth.isGCE); - const scope = nockIsGCE(); - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - scope.done(); - }); + const expected = await (gcpMetadata.getGCPResidency() || + gcpMetadata.isAvailable()); - it('_checkIsGCE does not execute the second time when not running on GCE', async () => { - assert.notStrictEqual(true, auth.isGCE); - const scope = nockNotGCE(); - await auth._checkIsGCE(); - assert.strictEqual(false as boolean, auth.isGCE); + assert.strict.notEqual(auth.isGCE, true); await auth._checkIsGCE(); - assert.strictEqual(false as boolean, auth.isGCE); - scope.done(); + assert.strictEqual(auth.isGCE, expected); }); it('getCredentials should get metadata from the server when running on GCE', async () => { From 7373b75c2205876f38ec2697f23758176a327fc7 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 29 Jun 2023 16:32:12 +0200 Subject: [PATCH 430/662] fix(deps): update dependency @googleapis/iam to v8 (#1581) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index cd617b67..8730ca59 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^6.0.0", - "@googleapis/iam": "^7.0.0", + "@googleapis/iam": "^8.0.0", "google-auth-library": "^8.8.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", From dde577f995b3a0b04e466d07f3a9a01a9e74af43 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 29 Jun 2023 08:56:41 -0700 Subject: [PATCH 431/662] chore(main): release 8.9.0 (#1549) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da482049..1f640e27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [8.9.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.8.0...v8.9.0) (2023-06-29) + + +### Features + +* Adds universe_domain field to base external client ([#1548](https://github.com/googleapis/google-auth-library-nodejs/issues/1548)) ([7412d7c](https://github.com/googleapis/google-auth-library-nodejs/commit/7412d7c55c688601199b5201b29cfbe63c4c7a70)) +* Utilize `gcp-metadata`'s GCP Residency Check ([#1513](https://github.com/googleapis/google-auth-library-nodejs/issues/1513)) ([43377ac](https://github.com/googleapis/google-auth-library-nodejs/commit/43377ac7af1ece14b82b053b504449f257d11af6)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v8 ([#1581](https://github.com/googleapis/google-auth-library-nodejs/issues/1581)) ([7373b75](https://github.com/googleapis/google-auth-library-nodejs/commit/7373b75c2205876f38ec2697f23758176a327fc7)) +* **deps:** Update dependency puppeteer to v20 ([#1550](https://github.com/googleapis/google-auth-library-nodejs/issues/1550)) ([3563c16](https://github.com/googleapis/google-auth-library-nodejs/commit/3563c16e2376729e399fefc0b1fad7bff78c296e)) +* IdTokenClient expiry_date check ([#1555](https://github.com/googleapis/google-auth-library-nodejs/issues/1555)) ([efcdef1](https://github.com/googleapis/google-auth-library-nodejs/commit/efcdef190fd94537d79087e7fcb26e5925a46173)) +* KeepAlive sample ([#1561](https://github.com/googleapis/google-auth-library-nodejs/issues/1561)) ([e3ab824](https://github.com/googleapis/google-auth-library-nodejs/commit/e3ab8247761b0a61f25e08a1b8fd6b200a4be200)) +* Make universeDomain private ([#1552](https://github.com/googleapis/google-auth-library-nodejs/issues/1552)) ([73b63d5](https://github.com/googleapis/google-auth-library-nodejs/commit/73b63d56fad8da033307e1be501a8846a5311211)) +* Remove auth lib version from x-goog-api-client header ([#1558](https://github.com/googleapis/google-auth-library-nodejs/issues/1558)) ([ce6013c](https://github.com/googleapis/google-auth-library-nodejs/commit/ce6013c6132ba2b82a1f8cba0a15c4710536204c)) + ## [8.8.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.7.0...v8.8.0) (2023-04-25) diff --git a/package.json b/package.json index bf2331bd..54168490 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.8.0", + "version": "8.9.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 8730ca59..74193cb3 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^8.0.0", - "google-auth-library": "^8.8.0", + "google-auth-library": "^8.9.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 5b9adefc3c5855f0643fd6fd4cd1e635a02a235b Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:01:48 -0700 Subject: [PATCH 432/662] chore: upgrade to Node 14 (#1584) Source-Link: https://github.com/googleapis/synthtool/commit/2d2d5e5c4e0eb30b0a7c2c95576e4e89c8443b35 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:bfa6fdba19aa7d105167d01fb51f5fd8285e8cd9fca264e43aff849e9e7fa36c Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/common.cfg | 2 +- .kokoro/continuous/node14/common.cfg | 24 ++++++++++++++++++++++ .kokoro/continuous/node14/lint.cfg | 4 ++++ .kokoro/continuous/node14/samples-test.cfg | 12 +++++++++++ .kokoro/continuous/node14/system-test.cfg | 12 +++++++++++ .kokoro/continuous/node14/test.cfg | 0 .kokoro/presubmit/node14/common.cfg | 24 ++++++++++++++++++++++ .kokoro/presubmit/node14/samples-test.cfg | 12 +++++++++++ .kokoro/presubmit/node14/system-test.cfg | 12 +++++++++++ .kokoro/presubmit/node14/test.cfg | 0 .kokoro/release/docs.cfg | 2 +- .kokoro/release/publish.cfg | 2 +- .kokoro/samples-test.sh | 2 +- .kokoro/system-test.sh | 2 +- .kokoro/test.sh | 2 +- 16 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 .kokoro/continuous/node14/common.cfg create mode 100644 .kokoro/continuous/node14/lint.cfg create mode 100644 .kokoro/continuous/node14/samples-test.cfg create mode 100644 .kokoro/continuous/node14/system-test.cfg create mode 100644 .kokoro/continuous/node14/test.cfg create mode 100644 .kokoro/presubmit/node14/common.cfg create mode 100644 .kokoro/presubmit/node14/samples-test.cfg create mode 100644 .kokoro/presubmit/node14/system-test.cfg create mode 100644 .kokoro/presubmit/node14/test.cfg diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 21ad18bd..9e959ba1 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:0527a86c10b67742c409dc726ba9a31ec4e69b0006e3d7a49b0e6686c59cdaa9 -# created: 2023-05-24T20:32:43.844586914Z + digest: sha256:bfa6fdba19aa7d105167d01fb51f5fd8285e8cd9fca264e43aff849e9e7fa36c +# created: 2023-07-06T17:45:12.014855061Z diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 8e9e508e..eb5e5c10 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -16,7 +16,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/continuous/node14/common.cfg b/.kokoro/continuous/node14/common.cfg new file mode 100644 index 00000000..eb5e5c10 --- /dev/null +++ b/.kokoro/continuous/node14/common.cfg @@ -0,0 +1,24 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" +} +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/test.sh" +} diff --git a/.kokoro/continuous/node14/lint.cfg b/.kokoro/continuous/node14/lint.cfg new file mode 100644 index 00000000..49ffcd82 --- /dev/null +++ b/.kokoro/continuous/node14/lint.cfg @@ -0,0 +1,4 @@ +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/lint.sh" +} diff --git a/.kokoro/continuous/node14/samples-test.cfg b/.kokoro/continuous/node14/samples-test.cfg new file mode 100644 index 00000000..9571f5db --- /dev/null +++ b/.kokoro/continuous/node14/samples-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/samples-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/continuous/node14/system-test.cfg b/.kokoro/continuous/node14/system-test.cfg new file mode 100644 index 00000000..83d64098 --- /dev/null +++ b/.kokoro/continuous/node14/system-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/system-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/continuous/node14/test.cfg b/.kokoro/continuous/node14/test.cfg new file mode 100644 index 00000000..e69de29b diff --git a/.kokoro/presubmit/node14/common.cfg b/.kokoro/presubmit/node14/common.cfg new file mode 100644 index 00000000..eb5e5c10 --- /dev/null +++ b/.kokoro/presubmit/node14/common.cfg @@ -0,0 +1,24 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" +} +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/test.sh" +} diff --git a/.kokoro/presubmit/node14/samples-test.cfg b/.kokoro/presubmit/node14/samples-test.cfg new file mode 100644 index 00000000..9571f5db --- /dev/null +++ b/.kokoro/presubmit/node14/samples-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/samples-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node14/system-test.cfg b/.kokoro/presubmit/node14/system-test.cfg new file mode 100644 index 00000000..83d64098 --- /dev/null +++ b/.kokoro/presubmit/node14/system-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/system-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node14/test.cfg b/.kokoro/presubmit/node14/test.cfg new file mode 100644 index 00000000..e69de29b diff --git a/.kokoro/release/docs.cfg b/.kokoro/release/docs.cfg index 02c51cf2..54d2a854 100644 --- a/.kokoro/release/docs.cfg +++ b/.kokoro/release/docs.cfg @@ -11,7 +11,7 @@ before_action { # doc publications use a Python image. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" } # Download trampoline resources. diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index 2479b117..8626c14d 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -30,7 +30,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" } env_vars: { diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index 806c0082..8c5d108c 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -56,7 +56,7 @@ fi # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=12 +COVERAGE_NODE=14 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 0201e9df..0b3043d2 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -49,7 +49,7 @@ npm run system-test # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=12 +COVERAGE_NODE=14 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/test.sh b/.kokoro/test.sh index a5c7ac04..862d478d 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -39,7 +39,7 @@ npm test # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=12 +COVERAGE_NODE=14 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then From d736da3fa5536650ae6a3aebbcae408254ebd035 Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Fri, 7 Jul 2023 11:38:06 -0700 Subject: [PATCH 433/662] build!: remove arrify and fast-text-encoding (#1583) * build!: remove arrify and fast-text-encoding --- browser-test/test.crypto.ts | 5 ----- browser-test/test.oauth2.ts | 5 ----- package.json | 2 -- src/auth/computeclient.ts | 7 +++++-- src/crypto/browser/crypto.ts | 8 -------- test/test.crypto.ts | 6 ------ 6 files changed, 5 insertions(+), 28 deletions(-) diff --git a/browser-test/test.crypto.ts b/browser-test/test.crypto.ts index 5da17f98..37e2d5f2 100644 --- a/browser-test/test.crypto.ts +++ b/browser-test/test.crypto.ts @@ -19,11 +19,6 @@ import {BrowserCrypto} from '../src/crypto/browser/crypto'; import {privateKey, publicKey} from './fixtures/keys'; import {describe, it} from 'mocha'; -// Not all browsers support `TextEncoder`. The following `require` will -// provide a fast UTF8-only replacement for those browsers that don't support -// text encoding natively. -require('fast-text-encoding'); - describe('Browser crypto tests', () => { const crypto = createCrypto(); diff --git a/browser-test/test.oauth2.ts b/browser-test/test.oauth2.ts index ffcb79c9..5626575d 100644 --- a/browser-test/test.oauth2.ts +++ b/browser-test/test.oauth2.ts @@ -18,11 +18,6 @@ import * as sinon from 'sinon'; import {privateKey, publicKey} from './fixtures/keys'; import {it, describe, beforeEach} from 'mocha'; -// Not all browsers support `TextEncoder`. The following `require` will -// provide a fast UTF8-only replacement for those browsers that don't support -// text encoding natively. -require('fast-text-encoding'); - import {CodeChallengeMethod, OAuth2Client} from '../src'; import {CertificateFormat} from '../src/auth/oauth2client'; import {JwkCertificate} from '../src/crypto/crypto'; diff --git a/package.json b/package.json index 54168490..eb2846bb 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,8 @@ "client library" ], "dependencies": { - "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", "gaxios": "^5.0.0", "gcp-metadata": "^5.3.0", "gtoken": "^6.1.0", diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index db91df01..acc7aeb1 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import arrify = require('arrify'); import {GaxiosError} from 'gaxios'; import * as gcpMetadata from 'gcp-metadata'; @@ -49,7 +48,11 @@ export class Compute extends OAuth2Client { // refreshed before the first API call is made. this.credentials = {expiry_date: 1, refresh_token: 'compute-placeholder'}; this.serviceAccountEmail = options.serviceAccountEmail || 'default'; - this.scopes = arrify(options.scopes); + this.scopes = Array.isArray(options.scopes) + ? options.scopes + : options.scopes + ? [options.scopes] + : []; } /** diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index feba104a..e46096f7 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -18,14 +18,6 @@ import * as base64js from 'base64-js'; -// Not all browsers support `TextEncoder`. The following `require` will -// provide a fast UTF8-only replacement for those browsers that don't support -// text encoding natively. -// eslint-disable-next-line node/no-unsupported-features/node-builtins -if (typeof process === 'undefined' && typeof TextEncoder === 'undefined') { - require('fast-text-encoding'); -} - import {Crypto, JwkCertificate, fromArrayBufferToHex} from '../crypto'; export class BrowserCrypto implements Crypto { diff --git a/test/test.crypto.ts b/test/test.crypto.ts index 101d94c8..3488c732 100644 --- a/test/test.crypto.ts +++ b/test/test.crypto.ts @@ -104,12 +104,6 @@ describe('crypto', () => { assert.strictEqual(encodedString, base64String); }); - it('should not load fast-text-encoding while running in nodejs', () => { - const loadedModules = Object.keys(require('module')._cache); - const hits = loadedModules.filter(x => x.includes('fast-text-encoding')); - assert.strictEqual(hits.length, 0); - }); - it('should calculate SHA256 digest in hex encoding', async () => { const input = 'I can calculate SHA256'; const expectedHexDigest = From 6004dca8d7e7aca7e570b56afd84d3c7f5d40242 Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:52:42 -0700 Subject: [PATCH 434/662] chore!: migrate to Node 14 (#1582) * chore!: migrate to Node 14 --------- Co-authored-by: Owl Bot --- .github/sync-repo-settings.yaml | 2 +- .github/workflows/ci.yaml | 2 +- .kokoro/continuous/node12/common.cfg | 24 ---------------------- .kokoro/continuous/node12/lint.cfg | 4 ---- .kokoro/continuous/node12/samples-test.cfg | 12 ----------- .kokoro/continuous/node12/system-test.cfg | 12 ----------- .kokoro/continuous/node12/test.cfg | 0 .kokoro/presubmit/node12/common.cfg | 24 ---------------------- .kokoro/presubmit/node12/samples-test.cfg | 12 ----------- .kokoro/presubmit/node12/system-test.cfg | 12 ----------- .kokoro/presubmit/node12/test.cfg | 0 package.json | 6 +++--- samples/package.json | 2 +- 13 files changed, 6 insertions(+), 106 deletions(-) delete mode 100644 .kokoro/continuous/node12/common.cfg delete mode 100644 .kokoro/continuous/node12/lint.cfg delete mode 100644 .kokoro/continuous/node12/samples-test.cfg delete mode 100644 .kokoro/continuous/node12/system-test.cfg delete mode 100644 .kokoro/continuous/node12/test.cfg delete mode 100644 .kokoro/presubmit/node12/common.cfg delete mode 100644 .kokoro/presubmit/node12/samples-test.cfg delete mode 100644 .kokoro/presubmit/node12/system-test.cfg delete mode 100644 .kokoro/presubmit/node12/test.cfg diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 4a30a08e..1350faef 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -9,9 +9,9 @@ branchProtectionRules: - "ci/kokoro: System test" - docs - lint - - test (12) - test (14) - test (16) + - test (18) - cla/google - windows - OwlBot Post Processor diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2377ffbe..3932645f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [12, 14, 16] + node: [14, 16, 18] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/.kokoro/continuous/node12/common.cfg b/.kokoro/continuous/node12/common.cfg deleted file mode 100644 index 8e9e508e..00000000 --- a/.kokoro/continuous/node12/common.cfg +++ /dev/null @@ -1,24 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/test.sh" -} diff --git a/.kokoro/continuous/node12/lint.cfg b/.kokoro/continuous/node12/lint.cfg deleted file mode 100644 index 49ffcd82..00000000 --- a/.kokoro/continuous/node12/lint.cfg +++ /dev/null @@ -1,4 +0,0 @@ -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/lint.sh" -} diff --git a/.kokoro/continuous/node12/samples-test.cfg b/.kokoro/continuous/node12/samples-test.cfg deleted file mode 100644 index 9571f5db..00000000 --- a/.kokoro/continuous/node12/samples-test.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Download resources for system tests (service account key, etc.) -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/samples-test.sh" -} - -env_vars: { - key: "SECRET_MANAGER_KEYS" - value: "long-door-651-kokoro-system-test-service-account" -} \ No newline at end of file diff --git a/.kokoro/continuous/node12/system-test.cfg b/.kokoro/continuous/node12/system-test.cfg deleted file mode 100644 index 83d64098..00000000 --- a/.kokoro/continuous/node12/system-test.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Download resources for system tests (service account key, etc.) -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/system-test.sh" -} - -env_vars: { - key: "SECRET_MANAGER_KEYS" - value: "long-door-651-kokoro-system-test-service-account" -} \ No newline at end of file diff --git a/.kokoro/continuous/node12/test.cfg b/.kokoro/continuous/node12/test.cfg deleted file mode 100644 index e69de29b..00000000 diff --git a/.kokoro/presubmit/node12/common.cfg b/.kokoro/presubmit/node12/common.cfg deleted file mode 100644 index 8e9e508e..00000000 --- a/.kokoro/presubmit/node12/common.cfg +++ /dev/null @@ -1,24 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/test.sh" -} diff --git a/.kokoro/presubmit/node12/samples-test.cfg b/.kokoro/presubmit/node12/samples-test.cfg deleted file mode 100644 index 9571f5db..00000000 --- a/.kokoro/presubmit/node12/samples-test.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Download resources for system tests (service account key, etc.) -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/samples-test.sh" -} - -env_vars: { - key: "SECRET_MANAGER_KEYS" - value: "long-door-651-kokoro-system-test-service-account" -} \ No newline at end of file diff --git a/.kokoro/presubmit/node12/system-test.cfg b/.kokoro/presubmit/node12/system-test.cfg deleted file mode 100644 index 83d64098..00000000 --- a/.kokoro/presubmit/node12/system-test.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Download resources for system tests (service account key, etc.) -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/google-auth-library-nodejs/.kokoro/system-test.sh" -} - -env_vars: { - key: "SECRET_MANAGER_KEYS" - value: "long-door-651-kokoro-system-test-service-account" -} \ No newline at end of file diff --git a/.kokoro/presubmit/node12/test.cfg b/.kokoro/presubmit/node12/test.cfg deleted file mode 100644 index e69de29b..00000000 diff --git a/package.json b/package.json index eb2846bb..9913fd54 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { - "node": ">=12" + "node": ">=14" }, "main": "./build/src/index.js", "types": "./build/src/index.d.ts", @@ -41,7 +41,7 @@ "chai": "^4.2.0", "codecov": "^3.0.2", "execa": "^5.0.0", - "gts": "^3.1.0", + "gts": "^3.1.1", "is-docker": "^2.0.0", "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", @@ -60,7 +60,7 @@ "puppeteer": "^20.0.0", "sinon": "^15.0.0", "ts-loader": "^8.0.0", - "typescript": "^4.6.3", + "typescript": "^5.1.6", "webpack": "^5.21.2", "webpack-cli": "^4.0.0" }, diff --git a/samples/package.json b/samples/package.json index 74193cb3..1522da45 100644 --- a/samples/package.json +++ b/samples/package.json @@ -9,7 +9,7 @@ "test": "mocha --timeout 60000" }, "engines": { - "node": ">=12" + "node": ">=14" }, "license": "Apache-2.0", "dependencies": { From 476b685a21d20601934d531a899a5e395b6d3d8e Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:11:34 -0700 Subject: [PATCH 435/662] build: add extra test for Node 20, update windows tests (#1586) * build: add extra test for Node 20, update windows tests Source-Link: https://github.com/googleapis/synthtool/commit/38f5d4bfd5d51116a3cf7f260b8fe5d8a0046cfa Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:ef104a520c849ffde60495342ecf099dfb6256eab0fbd173228f447bc73d1aa9 --------- Co-authored-by: Owl Bot Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 2 +- .kokoro/test.bat | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 9e959ba1..de4fa0a5 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:bfa6fdba19aa7d105167d01fb51f5fd8285e8cd9fca264e43aff849e9e7fa36c -# created: 2023-07-06T17:45:12.014855061Z + digest: sha256:ef104a520c849ffde60495342ecf099dfb6256eab0fbd173228f447bc73d1aa9 +# created: 2023-07-10T21:36:52.433664553Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3932645f..6451c81a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [14, 16, 18] + node: [14, 16, 18, 20] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/.kokoro/test.bat b/.kokoro/test.bat index ae59e59b..0bb12405 100644 --- a/.kokoro/test.bat +++ b/.kokoro/test.bat @@ -21,7 +21,7 @@ cd .. @rem we upgrade Node.js in the image: SET PATH=%PATH%;/cygdrive/c/Program Files/nodejs/npm -call nvm use v12.14.1 +call nvm use v14.17.3 call which node call npm install || goto :error From 11241e46784127f291d96a3e255a3470effa36ea Mon Sep 17 00:00:00 2001 From: Quentin Barbe Date: Tue, 11 Jul 2023 00:56:58 +0200 Subject: [PATCH 436/662] chore: clean up the Transporter interface. (#1407) The callback signature doesn't seem to be used. Co-authored-by: Benjamin E. Coe Co-authored-by: Daniel Bankhead Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- src/transporters.ts | 39 ++++--------------------- test/test.transporters.ts | 61 ++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 63 deletions(-) diff --git a/src/transporters.ts b/src/transporters.ts index 0b4e2617..47434c69 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -28,11 +28,6 @@ const PRODUCT_NAME = 'google-api-nodejs-client'; export interface Transporter { request(opts: GaxiosOptions): GaxiosPromise; - request(opts: GaxiosOptions, callback?: BodyResponseCallback): void; - request( - opts: GaxiosOptions, - callback?: BodyResponseCallback - ): GaxiosPromise | void; } export interface BodyResponseCallback { @@ -82,38 +77,14 @@ export class DefaultTransporter implements Transporter { * @param callback optional callback that contains GaxiosResponse object. * @return GaxiosPromise, assuming no callback is passed. */ - request(opts: GaxiosOptions): GaxiosPromise; - request(opts: GaxiosOptions, callback?: BodyResponseCallback): void; - request( - opts: GaxiosOptions, - callback?: BodyResponseCallback - ): GaxiosPromise | void { + request(opts: GaxiosOptions): GaxiosPromise { // ensure the user isn't passing in request-style options opts = this.configure(opts); - try { - validate(opts); - } catch (e) { - if (callback) { - return callback(e as Error); - } else { - throw e; - } - } + validate(opts); - if (callback) { - request(opts).then( - r => { - callback(null, r); - }, - e => { - callback(this.processError(e)); - } - ); - } else { - return request(opts).catch(e => { - throw this.processError(e); - }); - } + return request(opts).catch(e => { + throw this.processError(e); + }); } /** diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 97a72465..89559f4d 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -89,35 +89,33 @@ describe('transporters', () => { const scope = nock(url) .get('/') .reply(400, {error: {code: 500, errors: [firstError, secondError]}}); - transporter.request({url}, error => { - scope.done(); - assert.strictEqual(error!.message, 'Error 1\nError 2'); - assert.strictEqual((error as RequestError).code, 500); - assert.strictEqual((error as RequestError).errors.length, 2); - done(); - }); + transporter.request({url}).then( + () => { + scope.done(); + done('Unexpected promise success'); + }, + error => { + scope.done(); + assert.strictEqual(error!.message, 'Error 1\nError 2'); + assert.strictEqual((error as RequestError).code, 500); + assert.strictEqual((error as RequestError).errors.length, 2); + done(); + } + ); }); it('should return an error for a 404 response', done => { const url = 'http://example.com'; const scope = nock(url).get('/').reply(404, 'Not found'); - transporter.request({url}, error => { - scope.done(); - assert.strictEqual(error!.message, 'Not found'); - assert.strictEqual((error as RequestError).code, '404'); - done(); - }); - }); - - it('should return an error if you try to use request config options', done => { - const expected = - "'uri' is not a valid configuration option. Please use 'url' instead. This library is using Axios for requests. Please see https://github.com/axios/axios to learn more about the valid request options."; - transporter.request( - { - uri: 'http://example.com/api', - } as GaxiosOptions, + transporter.request({url}).then( + () => { + scope.done(); + done('Unexpected promise success'); + }, error => { - assert.strictEqual(error!.message, expected); + scope.done(); + assert.strictEqual(error!.message, 'Not found'); + assert.strictEqual((error as RequestError).code, '404'); done(); } ); @@ -151,12 +149,17 @@ describe('transporters', () => { it('should work with a callback', done => { const url = 'http://example.com'; const scope = nock(url).get('/').reply(200); - transporter.request({url}, (err, res) => { - scope.done(); - assert.strictEqual(err, null); - assert.strictEqual(res!.status, 200); - done(); - }); + transporter.request({url}).then( + res => { + scope.done(); + assert.strictEqual(res!.status, 200); + done(); + }, + _error => { + scope.done(); + done('Unexpected promise failure'); + } + ); }); // tslint:disable-next-line ban From dfac5259fa573f25960949ae1c2f98bc78962717 Mon Sep 17 00:00:00 2001 From: Quentin Barbe Date: Tue, 11 Jul 2023 01:34:31 +0200 Subject: [PATCH 437/662] feat!: make transporter attribute type more generic (#1406) * feat: make tranporter attribute type more generic * fix: keep `quotaProjectId` public * fix: Use `Transporter` instead of `DefaultTransporter` --------- Co-authored-by: Benjamin E. Coe Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> Co-authored-by: Daniel Bankhead --- src/auth/authclient.ts | 4 ++-- src/auth/externalAccountAuthorizedUserClient.ts | 4 ++-- src/auth/stscredentials.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 3e443334..f41a1537 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -15,7 +15,7 @@ import {EventEmitter} from 'events'; import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; -import {DefaultTransporter} from '../transporters'; +import {DefaultTransporter, Transporter} from '../transporters'; import {Credentials} from './credentials'; import {Headers} from './oauth2client'; @@ -93,7 +93,7 @@ export abstract class AuthClient * See {@link https://cloud.google.com/docs/quota| Working with quotas} */ quotaProjectId?: string; - transporter = new DefaultTransporter(); + transporter: Transporter = new DefaultTransporter(); credentials: Credentials = {}; projectId?: string | null; eagerRefreshThresholdMillis = 5 * 60 * 1000; diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index c0c081cd..ca267bda 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -20,7 +20,7 @@ import { OAuthClientAuthHandler, OAuthErrorResponse, } from './oauth2common'; -import {BodyResponseCallback, DefaultTransporter} from '../transporters'; +import {BodyResponseCallback, Transporter} from '../transporters'; import { GaxiosError, GaxiosOptions, @@ -83,7 +83,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { */ constructor( private readonly url: string, - private readonly transporter: DefaultTransporter, + private readonly transporter: Transporter, clientAuthentication?: ClientAuthentication ) { super(clientAuthentication); diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 52178b82..19ec95bd 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -15,7 +15,7 @@ import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; import * as querystring from 'querystring'; -import {DefaultTransporter} from '../transporters'; +import {DefaultTransporter, Transporter} from '../transporters'; import {Headers} from './oauth2client'; import { ClientAuthentication, @@ -131,7 +131,7 @@ export interface StsSuccessfulResponse { * https://tools.ietf.org/html/rfc8693 */ export class StsCredentials extends OAuthClientAuthHandler { - private transporter: DefaultTransporter; + private transporter: Transporter; /** * Initializes an STS credentials instance. From f95a153409735a1f4c470fd7de18bf9ab5d5e771 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 11 Jul 2023 20:55:56 +0200 Subject: [PATCH 438/662] fix(deps): update dependency @googleapis/iam to v10 (#1588) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 1522da45..62da47c4 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^6.0.0", - "@googleapis/iam": "^8.0.0", + "@googleapis/iam": "^10.0.0", "google-auth-library": "^8.9.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 1bf3376eefda661b14be82bd28aa5057e4d4d054 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:39:14 -0700 Subject: [PATCH 439/662] build: Fixing tests that use reqheaders improperly (#1589) * fix: Fixing tests that use reqheaders improperly * fix lint --- test/externalclienthelper.ts | 71 ++- test/test.awsclient.ts | 15 +- test/test.baseexternalclient.ts | 430 +++++++++--------- test/test.downscopedclient.ts | 74 +-- ...est.externalaccountauthorizeduserclient.ts | 112 ++--- test/test.googleauth.ts | 14 +- test/test.identitypoolclient.ts | 234 +++++----- test/test.oauth2.ts | 164 +++---- 8 files changed, 553 insertions(+), 561 deletions(-) diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 0206aefa..ea96872c 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -36,7 +36,6 @@ interface NockMockStsToken { response: StsSuccessfulResponse | OAuthErrorResponse; // eslint-disable-next-line @typescript-eslint/no-explicit-any request: {[key: string]: any}; - additionalHeaders?: {[key: string]: string}; } interface NockMockGenerateAccessToken { @@ -58,51 +57,43 @@ const saBaseUrl = 'https://iamcredentials.googleapis.com'; const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; export function mockStsTokenExchange( - nockParams: NockMockStsToken[] + nockParams: NockMockStsToken[], + additionalHeaders?: {[key: string]: string} ): nock.Scope { - const scope = nock(baseUrl); + const headers = Object.assign( + { + 'content-type': 'application/x-www-form-urlencoded', + }, + additionalHeaders || {} + ); + const scope = nock(baseUrl, {reqheaders: headers}); nockParams.forEach(nockMockStsToken => { - const headers = Object.assign( - { - 'content-type': 'application/x-www-form-urlencoded', - }, - nockMockStsToken.additionalHeaders || {} - ); scope - .post(path, qs.stringify(nockMockStsToken.request), { - reqheaders: headers, - }) + .post(path, qs.stringify(nockMockStsToken.request)) .reply(nockMockStsToken.statusCode, nockMockStsToken.response); }); return scope; } export function mockGenerateAccessToken( - nockParams: NockMockGenerateAccessToken[] + nockMockGenerateAccessToken: NockMockGenerateAccessToken ): nock.Scope { - const scope = nock(saBaseUrl); - nockParams.forEach(nockMockGenerateAccessToken => { - const token = nockMockGenerateAccessToken.token; - scope - .post( - saPath, - { - scope: nockMockGenerateAccessToken.scopes, - lifetime: - (nockMockGenerateAccessToken.lifetime ?? defaultLifetime) + 's', - }, - { - reqheaders: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - } - ) - .reply( - nockMockGenerateAccessToken.statusCode, - nockMockGenerateAccessToken.response - ); + const token = nockMockGenerateAccessToken.token; + const scope = nock(saBaseUrl, { + reqheaders: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, }); + scope + .post(saPath, { + scope: nockMockGenerateAccessToken.scopes, + lifetime: (nockMockGenerateAccessToken.lifetime ?? defaultLifetime) + 's', + }) + .reply( + nockMockGenerateAccessToken.statusCode, + nockMockGenerateAccessToken.response + ); return scope; } @@ -135,11 +126,9 @@ export function mockCloudResourceManager( statusCode: number, response: ProjectInfo | CloudRequestError ): nock.Scope { - return nock('https://cloudresourcemanager.googleapis.com') - .get(`/v1/projects/${projectNumber}`, undefined, { - reqheaders: { - Authorization: `Bearer ${accessToken}`, - }, - }) + return nock('https://cloudresourcemanager.googleapis.com', { + reqheaders: {Authorization: `Bearer ${accessToken}`}, + }) + .get(`/v1/projects/${projectNumber}`) .reply(statusCode, response); } diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 58e9c8a1..bea72deb 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -181,6 +181,7 @@ describe('AwsClient', () => { if (clock) { clock.restore(); } + nock.cleanAll(); }); it('should be a subclass of ExternalAccountClient', () => { @@ -574,14 +575,12 @@ describe('AwsClient', () => { }, }, ]), - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new AwsClient(awsOptionsWithSA); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 917ff6b4..2ee050bc 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -528,27 +528,29 @@ describe('BaseExternalAccountClient', () => { }); it('should use client auth over passing the workforce user project when both are provided', async () => { - const scope = mockStsTokenExchange([ - { - statusCode: 200, - response: stsSuccessfulResponse, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience: - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token_0', - subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - }, - additionalHeaders: { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds - )}`, + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + }, }, - }, - ]); + ], + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + } + ); const client = new TestExternalAccountClient( externalAccountOptionsWithClientAuthAndWorkforceUserProject @@ -601,27 +603,29 @@ describe('BaseExternalAccountClient', () => { }); it('should not throw if client auth is provided but workforce user project is not', async () => { - const scope = mockStsTokenExchange([ - { - statusCode: 200, - response: stsSuccessfulResponse, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience: - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token_0', - subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - }, - additionalHeaders: { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds - )}`, + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + }, }, - }, - ]); + ], + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + } + ); const externalAccountOptionsWithClientAuth: BaseExternalAccountClientOptions = Object.assign( {}, @@ -980,26 +984,28 @@ describe('BaseExternalAccountClient', () => { }); it('should apply basic auth when credentials are provided', async () => { - const scope = mockStsTokenExchange([ - { - statusCode: 200, - response: stsSuccessfulResponse, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience, - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token_0', - subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', - }, - additionalHeaders: { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds - )}`, + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, }, - }, - ]); + ], + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + } + ); const client = new TestExternalAccountClient( externalAccountOptionsWithCreds @@ -1050,14 +1056,12 @@ describe('BaseExternalAccountClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new TestExternalAccountClient( @@ -1109,20 +1113,20 @@ describe('BaseExternalAccountClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: saErrorResponse.error.code, - response: saErrorResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse2.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: saErrorResponse.error.code, + response: saErrorResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) + ); + scopes.push( + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse2.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new TestExternalAccountClient( @@ -1163,14 +1167,12 @@ describe('BaseExternalAccountClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['scope1', 'scope2'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['scope1', 'scope2'], + }) ); const client = new TestExternalAccountClient( @@ -1237,20 +1239,20 @@ describe('BaseExternalAccountClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - { - statusCode: 200, - response: saSuccessResponse2, - token: stsSuccessfulResponse2.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) + ); + scopes.push( + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse2, + token: stsSuccessfulResponse2.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new TestExternalAccountClient( @@ -1361,20 +1363,20 @@ describe('BaseExternalAccountClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - { - statusCode: 200, - response: saSuccessResponse2, - token: stsSuccessfulResponse2.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) + ); + scopes.push( + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse2, + token: stsSuccessfulResponse2.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new TestExternalAccountClient( @@ -1421,36 +1423,36 @@ describe('BaseExternalAccountClient', () => { it('should apply basic auth when credentials are provided', async () => { const scopes: nock.Scope[] = []; scopes.push( - mockStsTokenExchange([ - { - statusCode: 200, - response: stsSuccessfulResponse, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience, - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token_0', - subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', - }, - additionalHeaders: { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds - )}`, + mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, }, - }, - ]) + ], + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + } + ) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new TestExternalAccountClient( @@ -1492,14 +1494,12 @@ describe('BaseExternalAccountClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new TestExternalAccountClient( @@ -1536,15 +1536,13 @@ describe('BaseExternalAccountClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - lifetime: 2800, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + lifetime: 2800, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const externalAccountOptionsWithSATokenLifespan = Object.assign( @@ -1628,14 +1626,12 @@ describe('BaseExternalAccountClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new TestExternalAccountClient( @@ -1750,10 +1746,10 @@ describe('BaseExternalAccountClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -1813,18 +1809,16 @@ describe('BaseExternalAccountClient', () => { }, }, ]), - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }), + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -1875,10 +1869,10 @@ describe('BaseExternalAccountClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -1966,10 +1960,10 @@ describe('BaseExternalAccountClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -2020,10 +2014,10 @@ describe('BaseExternalAccountClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(400, errorMessage), ]; @@ -2095,14 +2089,15 @@ describe('BaseExternalAccountClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) - .reply(401) - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders2), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) + .reply(401), + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders2), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -2149,10 +2144,10 @@ describe('BaseExternalAccountClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(401), ]; @@ -2219,14 +2214,15 @@ describe('BaseExternalAccountClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) - .reply(403) - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders2), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) + .reply(403), + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders2), + }) + .post('/api', exampleRequest) .reply(403), ]; diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index 1b70b4e2..8ba7180c 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -832,10 +832,10 @@ describe('DownscopedClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -887,10 +887,10 @@ describe('DownscopedClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -982,10 +982,10 @@ describe('DownscopedClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -1036,10 +1036,10 @@ describe('DownscopedClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(400, errorMessage), ]; @@ -1111,14 +1111,15 @@ describe('DownscopedClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) - .reply(401) - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders2), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) + .reply(401), + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders2), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -1165,10 +1166,10 @@ describe('DownscopedClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(401), ]; @@ -1237,14 +1238,15 @@ describe('DownscopedClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) - .reply(403) - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders2), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) + .reply(403), + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders2), + }) + .post('/api', exampleRequest) .reply(403), ]; diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 879f48e9..5b5056a0 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -51,28 +51,29 @@ describe('ExternalAccountAuthorizedUserClient', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any request: {[key: string]: any}; times?: number; - additionalHeaders?: {[key: string]: string}; } function mockStsTokenRefresh( url: string, path: string, - nockParams: NockMockRefreshResponse[] + nockParams: NockMockRefreshResponse[], + additionalHeaders?: {[key: string]: string} ): nock.Scope { - const scope = nock(url); + const headers = Object.assign( + { + 'content-type': 'application/x-www-form-urlencoded', + }, + additionalHeaders || {} + ); + const scope = nock(url, { + reqheaders: headers, + }); + nockParams.forEach(nockMockStsToken => { const times = nockMockStsToken.times !== undefined ? nockMockStsToken.times : 1; - const headers = Object.assign( - { - 'content-type': 'application/x-www-form-urlencoded', - }, - nockMockStsToken.additionalHeaders || {} - ); scope - .post(path, qs.stringify(nockMockStsToken.request), { - reqheaders: headers, - }) + .post(path, qs.stringify(nockMockStsToken.request)) .times(times) .reply(nockMockStsToken.statusCode, nockMockStsToken.response); }); @@ -105,6 +106,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { }); afterEach(() => { + nock.cleanAll(); if (clock) { clock.restore(); } @@ -208,12 +210,12 @@ describe('ExternalAccountAuthorizedUserClient', () => { refresh_token: 'refreshToken', }); - const scope = nock(BASE_URL) - .post(REFRESH_PATH, expectedRequest.toString(), { - reqheaders: { - 'content-type': 'application/x-www-form-urlencoded', - }, - }) + const scope = nock(BASE_URL, { + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }) + .post(REFRESH_PATH, expectedRequest.toString()) .replyWithError({code: 'ETIMEDOUT'}); const client = new ExternalAccountAuthorizedUserClient( @@ -426,10 +428,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -511,10 +513,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -562,10 +564,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(400, errorMessage), ]; @@ -617,14 +619,15 @@ describe('ExternalAccountAuthorizedUserClient', () => { times: 2, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) - .reply(401) - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) + .reply(401), + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; @@ -669,10 +672,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(401), ]; @@ -722,14 +725,15 @@ describe('ExternalAccountAuthorizedUserClient', () => { times: 2, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) - .reply(403) - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, exampleHeaders, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) + .reply(403), + nock('https://example.com', { + reqheaders: Object.assign({}, exampleHeaders, authHeaders), + }) + .post('/api', exampleRequest) .reply(403), ]; @@ -777,10 +781,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { }, }, ]), - nock('https://example.com') - .post('/api', exampleRequest, { - reqheaders: Object.assign({}, authHeaders), - }) + nock('https://example.com', { + reqheaders: Object.assign({}, authHeaders), + }) + .post('/api', exampleRequest) .reply(200, Object.assign({}, exampleResponse)), ]; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index eca30109..d81af8da 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1713,14 +1713,12 @@ describe('googleauth', () => { ]; if (mockServiceAccountImpersonation) { scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: expectedScopes, - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: expectedScopes, + }) ); } diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 69b97cc8..1b6bde53 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; +import {afterEach, describe, it} from 'mocha'; import * as fs from 'fs'; import * as nock from 'nock'; import {createCrypto} from '../src/crypto/crypto'; @@ -169,6 +169,10 @@ describe('IdentityPoolClient', () => { scope: 'scope1 scope2', }; + afterEach(() => { + nock.cleanAll(); + }); + it('should be a subclass of BaseExternalAccountClient', () => { assert(IdentityPoolClient.prototype instanceof BaseExternalAccountClient); }); @@ -425,28 +429,30 @@ describe('IdentityPoolClient', () => { }); it('should resolve with the expected response on workforce configs with client auth', async () => { - const scope = mockStsTokenExchange([ - { - statusCode: 200, - response: stsSuccessfulResponse, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience: - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - // Subject token loaded from file should be used. - subject_token: fileSubjectToken, - subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - }, - additionalHeaders: { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds - )}`, + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + }, }, - }, - ]); + ], + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + } + ); const client = new IdentityPoolClient( fileSourcedOptionsWithClientAuthAndWorkforceUserProject @@ -500,27 +506,29 @@ describe('IdentityPoolClient', () => { }); it('should not throw if client auth is provided but workforce user project is not', async () => { - const scope = mockStsTokenExchange([ - { - statusCode: 200, - response: stsSuccessfulResponse, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience: - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: fileSubjectToken, - subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - }, - additionalHeaders: { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds - )}`, + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + }, }, - }, - ]); + ], + { + Authorization: `Basic ${crypto.encodeBase64StringUtf8( + basicAuthCreds + )}`, + } + ); const fileSourcedOptionsWithClientAuth: IdentityPoolClientOptions = Object.assign( {}, @@ -570,14 +578,12 @@ describe('IdentityPoolClient', () => { ]) ); scopes.push( - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new IdentityPoolClient( @@ -618,14 +624,12 @@ describe('IdentityPoolClient', () => { }, }, ]), - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new IdentityPoolClient(fileSourcedOptionsWithSA); @@ -694,14 +698,12 @@ describe('IdentityPoolClient', () => { }, }, ]), - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new IdentityPoolClient(jsonFileSourcedOptionsWithSA); @@ -734,10 +736,10 @@ describe('IdentityPoolClient', () => { describe('retrieveSubjectToken()', () => { it('should resolve on text response success', async () => { const externalSubjectToken = 'SUBJECT_TOKEN_1'; - const scope = nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + const scope = nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(200, externalSubjectToken); const client = new IdentityPoolClient(urlSourcedOptions); @@ -752,10 +754,10 @@ describe('IdentityPoolClient', () => { const jsonResponse = { access_token: externalSubjectToken, }; - const scope = nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + const scope = nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(200, jsonResponse); const client = new IdentityPoolClient(jsonRespUrlSourcedOptions); @@ -787,10 +789,10 @@ describe('IdentityPoolClient', () => { const jsonResponse = { access_token: externalSubjectToken, }; - const scope = nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + const scope = nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(200, jsonResponse); const client = new IdentityPoolClient(invalidOptions); @@ -817,10 +819,10 @@ describe('IdentityPoolClient', () => { }); it('should reject with underlying on non-200 response', async () => { - const scope = nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + const scope = nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(404); const client = new IdentityPoolClient(urlSourcedOptions); @@ -855,10 +857,10 @@ describe('IdentityPoolClient', () => { ]) ); scopes.push( - nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(200, externalSubjectToken) ); @@ -883,10 +885,10 @@ describe('IdentityPoolClient', () => { const externalSubjectToken = 'SUBJECT_TOKEN_1'; const scopes: nock.Scope[] = []; scopes.push( - nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(200, externalSubjectToken), mockStsTokenExchange([ { @@ -904,14 +906,12 @@ describe('IdentityPoolClient', () => { }, }, ]), - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new IdentityPoolClient(urlSourcedOptionsWithSA); @@ -951,10 +951,10 @@ describe('IdentityPoolClient', () => { ]) ); scopes.push( - nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(200, jsonResponse) ); @@ -982,10 +982,10 @@ describe('IdentityPoolClient', () => { }; const scopes: nock.Scope[] = []; scopes.push( - nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(200, jsonResponse), mockStsTokenExchange([ { @@ -1003,14 +1003,12 @@ describe('IdentityPoolClient', () => { }, }, ]), - mockGenerateAccessToken([ - { - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }, - ]) + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) ); const client = new IdentityPoolClient(jsonRespUrlSourcedOptionsWithSA); @@ -1026,10 +1024,10 @@ describe('IdentityPoolClient', () => { }); it('should reject with retrieveSubjectToken error', async () => { - const scope = nock(metadataBaseUrl) - .get(metadataPath, undefined, { - reqheaders: metadataHeaders, - }) + const scope = nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) .reply(404); const client = new IdentityPoolClient(urlSourcedOptions); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index a4946df9..453acd09 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -914,10 +914,10 @@ describe('oauth2', () => { function mockExample() { return [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) + nock(baseUrl, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, {access_token: 'abc123', expires_in: 1}), nock('http://example.com').get('/').reply(200), ]; @@ -948,10 +948,10 @@ describe('oauth2', () => { // Mock a single call to the token server, and 3 calls to the example // endpoint. This makes sure that refreshToken is called only once. const scopes = [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) + nock(baseUrl, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, {access_token: 'abc123', expires_in: 1}), nock('http://example.com').get('/').thrice().reply(200), ]; @@ -970,10 +970,10 @@ describe('oauth2', () => { // This makes sure that the token endpoint is invoked twice, preventing // the promise from getting cached for too long. const scopes = [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) + nock(baseUrl, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .twice() .reply(200, {access_token: 'abc123', expires_in: 100000}), nock('http://example.com').get('/').twice().reply(200), @@ -990,14 +990,12 @@ describe('oauth2', () => { // Mock a failed call to the refreshToken endpoint. This should trigger // a second call to refreshToken, which should use a different promise. const scopes = [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) + nock(baseUrl, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(500) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) + .post('/token') .reply(200, {access_token: 'abc123', expires_in: 100000}), nock('http://example.com').get('/').reply(200), ]; @@ -1020,10 +1018,10 @@ describe('oauth2', () => { }; const scopes = [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) + nock(baseUrl, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(500, reAuthErrorBody), ]; client.credentials = {refresh_token: 'refresh-token-placeholder'}; @@ -1138,15 +1136,16 @@ describe('oauth2', () => { .get('/access') .reply(code, { error: {code, message: 'Invalid Credentials'}, - }) - .get('/access', undefined, { - reqheaders: {Authorization: 'Bearer abc123'}, - }) + }), + nock('http://example.com', { + reqheaders: {Authorization: 'Bearer abc123'}, + }) + .get('/access') .reply(200), - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) + nock(baseUrl, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, {access_token: 'abc123', expires_in: 1000}), ]; client.credentials = { @@ -1170,19 +1169,20 @@ describe('oauth2', () => { forceRefreshOnFailure: true, }); const scopes = [ - nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, - }) + nock(baseUrl, { + reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, {access_token: 'abc123', expires_in: 1000}), nock('http://example.com') .get('/access') .reply(code, { error: {code, message: 'Invalid Credentials'}, - }) - .get('/access', undefined, { - reqheaders: {Authorization: 'Bearer abc123'}, - }) + }), + nock('http://example.com', { + reqheaders: {Authorization: 'Bearer abc123'}, + }) + .get('/access') .reply(200), ]; client.credentials = { @@ -1203,15 +1203,18 @@ describe('oauth2', () => { const authHeaders = { Authorization: 'Bearer access_token', }; - const scope = nock('http://example.com') - .get('/access') - .reply(code, { - error: {code, message: 'Invalid Credentials'}, - }) - .get('/access', undefined, { + const scopes = [ + nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }), + nock('http://example.com', { reqheaders: authHeaders, }) - .reply(200, {foo: 'bar'}); + .get('/access') + .reply(200, {foo: 'bar'}), + ]; const expectedRefreshedAccessToken = { access_token: 'access_token', expiry_date: new Date().getTime() + 3600 * 1000, @@ -1226,7 +1229,7 @@ describe('oauth2', () => { client.request({url: 'http://example.com/access'}, err => { assert.strictEqual(err, null); - scope.done(); + scopes.forEach(s => s.done()); assert.strictEqual( client.credentials.access_token, expectedRefreshedAccessToken.access_token @@ -1242,15 +1245,18 @@ describe('oauth2', () => { const authHeaders = { Authorization: 'Bearer access_token', }; - const scope = nock('http://example.com') - .get('/access') - .reply(code, { - error: {code, message: 'Invalid Credentials'}, - }) - .get('/access', undefined, { + const scopes = [ + nock('http://example.com') + .get('/access') + .reply(code, { + error: {code, message: 'Invalid Credentials'}, + }), + nock('http://example.com', { reqheaders: authHeaders, }) - .reply(200, {foo: 'bar'}); + .get('/access') + .reply(200, {foo: 'bar'}), + ]; const expectedRefreshedAccessToken = { access_token: 'access_token', expiry_date: new Date().getTime() + 3600 * 1000, @@ -1261,7 +1267,7 @@ describe('oauth2', () => { client.request({url: 'http://example.com/access'}, err => { assert.strictEqual(err, null); - scope.done(); + scopes.forEach(s => s.done()); assert.strictEqual( client.credentials.access_token, expectedRefreshedAccessToken.access_token @@ -1319,10 +1325,10 @@ describe('oauth2', () => { }); it('getToken should allow a code_verifier to be passed', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) + const scope = nock(baseUrl, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, { access_token: 'abc', refresh_token: '123', @@ -1340,10 +1346,10 @@ describe('oauth2', () => { }); it('getToken should set redirect_uri if not provided in options', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) + const scope = nock(baseUrl, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, { access_token: 'abc', refresh_token: '123', @@ -1358,10 +1364,10 @@ describe('oauth2', () => { }); it('getToken should set client_id if not provided in options', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) + const scope = nock(baseUrl, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, { access_token: 'abc', refresh_token: '123', @@ -1376,10 +1382,10 @@ describe('oauth2', () => { }); it('getToken should override redirect_uri if provided in options', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) + const scope = nock(baseUrl, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, { access_token: 'abc', refresh_token: '123', @@ -1397,10 +1403,10 @@ describe('oauth2', () => { }); it('getToken should override client_id if provided in options', async () => { - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) + const scope = nock(baseUrl, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, { access_token: 'abc', refresh_token: '123', @@ -1419,10 +1425,10 @@ describe('oauth2', () => { it('should return expiry_date', done => { const now = new Date().getTime(); - const scope = nock(baseUrl) - .post('/token', undefined, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, - }) + const scope = nock(baseUrl, { + reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + }) + .post('/token') .reply(200, { access_token: 'abc', refresh_token: '123', From 8738df9e24c62213b5a78e34a70cdcf456a794fa Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Wed, 12 Jul 2023 08:26:19 -0700 Subject: [PATCH 440/662] chore(deps)!: upgrade node-gtoken to 7.0.0 (#1590) * chore(deps): upgrade node-gtoken to 7.0.0 * fix!: remove p12 functionality for auth * revert * chore: remove tests --- package.json | 2 +- test/test.googleauth.ts | 57 ----------------------------------------- test/test.jwt.ts | 7 ----- 3 files changed, 1 insertion(+), 65 deletions(-) diff --git a/package.json b/package.json index 9913fd54..b9132e94 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^5.0.0", "gcp-metadata": "^5.3.0", - "gtoken": "^6.1.0", + "gtoken": "^7.0.0", "jws": "^4.0.0", "lru-cache": "^6.0.0" }, diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index d81af8da..c094ed42 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -2544,25 +2544,6 @@ describe('googleauth', () => { scope.done(); assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); }); - it('allows client to be instantiated from p12 key file', async () => { - const auth = new GoogleAuth({ - keyFile: P12_PATH, - clientOptions: { - scopes: 'http://foo', - email: 'foo@serviceaccount.com', - subject: 'bar@subjectaccount.com', - }, - }); - const jwt = await auth.getClient(); - const scope = createGTokenMock({access_token: 'initial-access-token'}); - const headers = await jwt.getRequestHeaders(); - assert.deepStrictEqual( - headers.Authorization, - 'Bearer initial-access-token' - ); - scope.done(); - assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); - }); // Allows a client to be instantiated from a certificate, // See: https://github.com/googleapis/google-auth-library-nodejs/issues/808 @@ -2585,25 +2566,6 @@ describe('googleauth', () => { scope.done(); assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); }); - it('allows client to be instantiated from p12 key file', async () => { - const auth = new GoogleAuth({ - keyFile: P12_PATH, - clientOptions: { - scopes: 'http://foo', - email: 'foo@serviceaccount.com', - subject: 'bar@subjectaccount.com', - }, - }); - const jwt = await auth.getClient(); - const scope = createGTokenMock({access_token: 'initial-access-token'}); - const headers = await jwt.getRequestHeaders(); - assert.deepStrictEqual( - headers.Authorization, - 'Bearer initial-access-token' - ); - scope.done(); - assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); - }); // Allows a client to be instantiated from a certificate, // See: https://github.com/googleapis/google-auth-library-nodejs/issues/808 @@ -2626,23 +2588,4 @@ describe('googleauth', () => { scope.done(); assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); }); - it('allows client to be instantiated from p12 key file', async () => { - const auth = new GoogleAuth({ - keyFile: P12_PATH, - clientOptions: { - scopes: 'http://foo', - email: 'foo@serviceaccount.com', - subject: 'bar@subjectaccount.com', - }, - }); - const jwt = await auth.getClient(); - const scope = createGTokenMock({access_token: 'initial-access-token'}); - const headers = await jwt.getRequestHeaders(); - assert.deepStrictEqual( - headers.Authorization, - 'Bearer initial-access-token' - ); - scope.done(); - assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); - }); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 6f94f281..f20fcbe5 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -721,13 +721,6 @@ describe('jwt', () => { assert.strictEqual(private_key, PEM_CONTENTS); }); - it('getCredentials should handle a p12 keyFile', async () => { - const jwt = new JWT({keyFile: P12_PATH}); - const {private_key, client_email} = await jwt.getCredentials(); - assert(private_key); - assert.strictEqual(client_email, undefined); - }); - it('getCredentials should handle a json keyFile', async () => { const jwt = new JWT(); jwt.fromJSON(json); From d8e5eb9994d2157a937543ed9f7d911b9e9db413 Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:30:57 -0700 Subject: [PATCH 441/662] build: update gaxios & gcp-metadata to 6.0.0, update @types/node to 20.4.2, decrease puppeteer to ^19.0.0, distinguish status and code in error handling. (#1592) * build: update gaxios & gcp-metadata to 6.0.0, update @types/node to 20.4.2, decrease puppeteer to ^19.0.0, distinguish status and code in error handling --- package.json | 8 ++++---- src/auth/computeclient.ts | 2 +- src/transporters.ts | 7 +++---- test/test.awsclient.ts | 12 ++++++------ test/test.baseexternalclient.ts | 4 ++-- test/test.downscopedclient.ts | 4 ++-- test/test.externalaccountauthorizeduserclient.ts | 4 ++-- test/test.identitypoolclient.ts | 4 ++-- test/test.oauth2.ts | 6 ++---- test/test.transporters.ts | 2 +- 10 files changed, 25 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index b9132e94..b80da56e 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^5.0.0", - "gcp-metadata": "^5.3.0", + "gaxios": "^6.0.0", + "gcp-metadata": "^6.0.0", "gtoken": "^7.0.0", "jws": "^4.0.0", "lru-cache": "^6.0.0" @@ -34,7 +34,7 @@ "@types/mocha": "^9.0.0", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", - "@types/node": "^16.0.0", + "@types/node": "^20.4.2", "@types/sinon": "^10.0.0", "assert-rejects": "^1.0.0", "c8": "^8.0.0", @@ -57,7 +57,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^20.0.0", + "puppeteer": "^19.0.0", "sinon": "^15.0.0", "ts-loader": "^8.0.0", "typescript": "^5.1.6", diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index acc7aeb1..f4bc7a8f 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -120,7 +120,7 @@ export class Compute extends OAuth2Client { protected wrapError(e: GaxiosError) { const res = e.response; if (res && res.status) { - e.code = res.status.toString(); + e.status = res.status; if (res.status === 403) { e.message = 'A Forbidden error was returned while attempting to retrieve an access ' + diff --git a/src/transporters.ts b/src/transporters.ts index 47434c69..26c1dab2 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -81,7 +81,6 @@ export class DefaultTransporter implements Transporter { // ensure the user isn't passing in request-style options opts = this.configure(opts); validate(opts); - return request(opts).catch(e => { throw this.processError(e); }); @@ -97,7 +96,7 @@ export class DefaultTransporter implements Transporter { if (res && body && body.error && res.status !== 200) { if (typeof body.error === 'string') { err.message = body.error; - err.code = res.status.toString(); + err.status = res.status; } else if (Array.isArray(body.error.errors)) { err.message = body.error.errors .map((err2: Error) => err2.message) @@ -106,12 +105,12 @@ export class DefaultTransporter implements Transporter { err.errors = body.error.errors; } else { err.message = body.error.message; - err.code = body.error.code || res.status; + err.code = body.error.code; } } else if (res && res.status >= 400) { // Consider all 4xx and 5xx responses errors. err.message = body; - err.code = res.status.toString(); + err.status = res.status; } return err; } diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index bea72deb..2d83126e 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -404,7 +404,7 @@ describe('AwsClient', () => { const client = new AwsClient(awsOptions); await assert.rejects(client.retrieveSubjectToken(), { - code: '500', + status: 500, }); scope.done(); }); @@ -420,7 +420,7 @@ describe('AwsClient', () => { const client = new AwsClient(awsOptions); await assert.rejects(client.retrieveSubjectToken(), { - code: '403', + status: 403, }); scope.done(); }); @@ -438,7 +438,7 @@ describe('AwsClient', () => { const client = new AwsClient(awsOptions); await assert.rejects(client.retrieveSubjectToken(), { - code: '408', + status: 408, }); scope.done(); }); @@ -605,7 +605,7 @@ describe('AwsClient', () => { const client = new AwsClient(awsOptions); await assert.rejects(client.getAccessToken(), { - code: '500', + status: 500, }); scope.done(); }); @@ -707,7 +707,7 @@ describe('AwsClient', () => { const client = new AwsClient(awsOptions); await assert.rejects(client.retrieveSubjectToken(), { - code: '500', + status: 500, }); scope.done(); }); @@ -985,7 +985,7 @@ describe('AwsClient', () => { const client = new AwsClient(awsOptions); await assert.rejects(client.getAccessToken(), { - code: '500', + status: 500, }); scope.done(); }); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 2ee050bc..f409ad58 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -2161,7 +2161,7 @@ describe('BaseExternalAccountClient', () => { responseType: 'json', }), { - code: '401', + status: 401, } ); @@ -2238,7 +2238,7 @@ describe('BaseExternalAccountClient', () => { responseType: 'json', }), { - code: '403', + status: 403, } ); diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index 8ba7180c..d280a9ee 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -1185,7 +1185,7 @@ describe('DownscopedClient', () => { responseType: 'json', }), { - code: '401', + status: 401, } ); @@ -1262,7 +1262,7 @@ describe('DownscopedClient', () => { responseType: 'json', }), { - code: '403', + status: 403, } ); scopes.forEach(scope => scope.done()); diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 5b5056a0..f9dcc45e 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -694,7 +694,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { responseType: 'json', }), { - code: '401', + status: 401, } ); @@ -752,7 +752,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { responseType: 'json', }), { - code: '403', + status: 403, } ); scopes.forEach(scope => scope.done()); diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 1b6bde53..c7383757 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -828,7 +828,7 @@ describe('IdentityPoolClient', () => { const client = new IdentityPoolClient(urlSourcedOptions); await assert.rejects(client.retrieveSubjectToken(), { - code: '404', + status: 404, }); scope.done(); }); @@ -1033,7 +1033,7 @@ describe('IdentityPoolClient', () => { const client = new IdentityPoolClient(urlSourcedOptions); await assert.rejects(client.getAccessToken(), { - code: '404', + status: 404, }); scope.done(); }); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 453acd09..752c7ee4 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -819,9 +819,8 @@ describe('oauth2', () => { .replyWithFile(200, certsResPath); client.getFederatedSignonCerts((err, certs) => { assert.strictEqual(err, null); - assert.strictEqual(Object.keys(certs!).length, 2); assert.notStrictEqual( - certs!.a15eea964ab9cce480e5ef4f47cb17b9fa7d0b21, + certs!['a15eea964ab9cce480e5ef4f47cb17b9fa7d0b21'], null ); assert.notStrictEqual( @@ -861,8 +860,7 @@ describe('oauth2', () => { .replyWithFile(200, pubkeysResPath); client.getIapPublicKeys((err, pubkeys) => { assert.strictEqual(err, null); - assert.strictEqual(Object.keys(pubkeys!).length, 2); - assert.notStrictEqual(pubkeys!.f9R3yg, null); + assert.notStrictEqual(pubkeys!['f9R3yg'], null); assert.notStrictEqual(pubkeys!['2nMJtw'], null); scope.done(); done(); diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 89559f4d..c1949bc5 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -115,7 +115,7 @@ describe('transporters', () => { error => { scope.done(); assert.strictEqual(error!.message, 'Not found'); - assert.strictEqual((error as RequestError).code, '404'); + assert.strictEqual((error as RequestError).status, 404); done(); } ); From 90ea4de1c03d7dd7fef2d7c8791c785c6c334fee Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 15:52:21 -0700 Subject: [PATCH 442/662] chore(main): release 9.0.0 (#1585) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f640e27..8f887cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.0.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.9.0...v9.0.0) (2023-07-20) + + +### ⚠ BREAKING CHANGES + +* **deps:** upgrade node-gtoken to 7.0.0 ([#1590](https://github.com/googleapis/google-auth-library-nodejs/issues/1590)) +* make transporter attribute type more generic ([#1406](https://github.com/googleapis/google-auth-library-nodejs/issues/1406)) +* migrate to Node 14 ([#1582](https://github.com/googleapis/google-auth-library-nodejs/issues/1582)) +* remove arrify and fast-text-encoding ([#1583](https://github.com/googleapis/google-auth-library-nodejs/issues/1583)) + +### Features + +* Make transporter attribute type more generic ([#1406](https://github.com/googleapis/google-auth-library-nodejs/issues/1406)) ([dfac525](https://github.com/googleapis/google-auth-library-nodejs/commit/dfac5259fa573f25960949ae1c2f98bc78962717)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v10 ([#1588](https://github.com/googleapis/google-auth-library-nodejs/issues/1588)) ([f95a153](https://github.com/googleapis/google-auth-library-nodejs/commit/f95a153409735a1f4c470fd7de18bf9ab5d5e771)) + + +### Build System + +* Remove arrify and fast-text-encoding ([#1583](https://github.com/googleapis/google-auth-library-nodejs/issues/1583)) ([d736da3](https://github.com/googleapis/google-auth-library-nodejs/commit/d736da3fa5536650ae6a3aebbcae408254ebd035)) + + +### Miscellaneous Chores + +* **deps:** Upgrade node-gtoken to 7.0.0 ([#1590](https://github.com/googleapis/google-auth-library-nodejs/issues/1590)) ([8738df9](https://github.com/googleapis/google-auth-library-nodejs/commit/8738df9e24c62213b5a78e34a70cdcf456a794fa)) +* Migrate to Node 14 ([#1582](https://github.com/googleapis/google-auth-library-nodejs/issues/1582)) ([6004dca](https://github.com/googleapis/google-auth-library-nodejs/commit/6004dca8d7e7aca7e570b56afd84d3c7f5d40242)) + ## [8.9.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.8.0...v8.9.0) (2023-06-29) diff --git a/package.json b/package.json index b80da56e..ef264553 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "8.9.0", + "version": "9.0.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 62da47c4..e476f224 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^6.0.0", "@googleapis/iam": "^10.0.0", - "google-auth-library": "^8.9.0", + "google-auth-library": "^9.0.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From fe09d6b4eb5a8215d21564a97bcc7ea5beb570bf Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:18:57 -0700 Subject: [PATCH 443/662] feature: Byoid metrics (#1595) * feat: adds byoid metrics logging * add tests * addressing PR comments --- src/auth/awsclient.ts | 1 + src/auth/baseexternalclient.ts | 29 +++++- src/auth/identitypoolclient.ts | 14 ++- src/auth/pluggable-auth-client.ts | 2 + test/externalclienthelper.ts | 12 +++ test/test.awsclient.ts | 50 ++++++++++ test/test.baseexternalclient.ts | 160 ++++++++++++++++++++++++++++++ test/test.identitypoolclient.ts | 115 ++++++++++++++++++++- test/test.pluggableauthclient.ts | 149 +++++++++++++++++++--------- 9 files changed, 480 insertions(+), 52 deletions(-) diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index 78575e07..4ab39182 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -100,6 +100,7 @@ export class AwsClient extends BaseExternalAccountClient { options.credential_source.imdsv2_session_token_url; this.awsRequestSigner = null; this.region = ''; + this.credentialSourceType = 'aws'; // Data validators. this.validateEnvironmentId(); diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 1aab7123..f29c2787 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -59,6 +59,9 @@ export const CLOUD_RESOURCE_MANAGER = const WORKFORCE_AUDIENCE_PATTERN = '//iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkg = require('../../../package.json'); + /** * Base external account credentials json interface. */ @@ -141,6 +144,8 @@ export abstract class BaseExternalAccountClient extends AuthClient { public projectNumber: string | null; public readonly eagerRefreshThresholdMillis: number; public readonly forceRefreshOnFailure: boolean; + private readonly configLifetimeRequested: boolean; + protected credentialSourceType?: string; /** * Instantiate a BaseExternalAccountClient instance using the provided JSON * object loaded from an external account credentials file. @@ -191,9 +196,15 @@ export abstract class BaseExternalAccountClient extends AuthClient { } this.serviceAccountImpersonationUrl = options.service_account_impersonation_url; + this.serviceAccountImpersonationLifetime = - options.service_account_impersonation?.token_lifetime_seconds ?? - DEFAULT_TOKEN_LIFESPAN; + options.service_account_impersonation?.token_lifetime_seconds; + if (this.serviceAccountImpersonationLifetime) { + this.configLifetimeRequested = true; + } else { + this.configLifetimeRequested = false; + this.serviceAccountImpersonationLifetime = DEFAULT_TOKEN_LIFESPAN; + } // As threshold could be zero, // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the // zero value. @@ -421,9 +432,12 @@ export abstract class BaseExternalAccountClient extends AuthClient { !this.clientAuth && this.workforcePoolUserProject ? {userProject: this.workforcePoolUserProject} : undefined; + const additionalHeaders: Headers = { + 'x-goog-api-client': this.getMetricsHeaderValue(), + }; const stsResponse = await this.stsCredential.exchangeToken( stsCredentialsOptions, - undefined, + additionalHeaders, additionalOptions ); @@ -544,4 +558,13 @@ export abstract class BaseExternalAccountClient extends AuthClient { return this.scopes; } } + + private getMetricsHeaderValue(): string { + const nodeVersion = process.version.replace(/^v/, ''); + const saImpersonation = this.serviceAccountImpersonationUrl !== undefined; + const credentialSourceType = this.credentialSourceType + ? this.credentialSourceType + : 'unknown'; + return `gl-node/${nodeVersion} auth/${pkg.version} google-byoid-sdk source/${credentialSourceType} sa-impersonation/${saImpersonation} config-lifetime/${this.configLifetimeRequested}`; + } } diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index 102025ed..38968bd5 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -86,8 +86,18 @@ export class IdentityPoolClient extends BaseExternalAccountClient { this.file = options.credential_source.file; this.url = options.credential_source.url; this.headers = options.credential_source.headers; - if (!this.file && !this.url) { - throw new Error('No valid Identity Pool "credential_source" provided'); + if (this.file && this.url) { + throw new Error( + 'No valid Identity Pool "credential_source" provided, must be either file or url.' + ); + } else if (this.file && !this.url) { + this.credentialSourceType = 'file'; + } else if (!this.file && this.url) { + this.credentialSourceType = 'url'; + } else { + throw new Error( + 'No valid Identity Pool "credential_source" provided, must be either file or url.' + ); } // Text is the default format type. this.formatType = options.credential_source.format?.type || 'text'; diff --git a/src/auth/pluggable-auth-client.ts b/src/auth/pluggable-auth-client.ts index d7be6ca8..8fb5a8f7 100644 --- a/src/auth/pluggable-auth-client.ts +++ b/src/auth/pluggable-auth-client.ts @@ -228,6 +228,8 @@ export class PluggableAuthClient extends BaseExternalAccountClient { timeoutMillis: this.timeoutMillis, outputFile: this.outputFile, }); + + this.credentialSourceType = 'executable'; } /** diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index ea96872c..81d190ef 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -56,6 +56,9 @@ export const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; const saBaseUrl = 'https://iamcredentials.googleapis.com'; const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkg = require('../../package.json'); + export function mockStsTokenExchange( nockParams: NockMockStsToken[], additionalHeaders?: {[key: string]: string} @@ -132,3 +135,12 @@ export function mockCloudResourceManager( .get(`/v1/projects/${projectNumber}`) .reply(statusCode, response); } + +export function getExpectedExternalAccountMetricsHeaderValue( + expectedSource: string, + expectedSaImpersonation: boolean, + expectedConfigLifetime: boolean +): string { + const languageVersion = process.version.replace(/^v/, ''); + return `gl-node/${languageVersion} auth/${pkg.version} google-byoid-sdk source/${expectedSource} sa-impersonation/${expectedSaImpersonation} config-lifetime/${expectedConfigLifetime}`; +} diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 2d83126e..9a99685c 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -26,6 +26,7 @@ import { getServiceAccountImpersonationUrl, mockGenerateAccessToken, mockStsTokenExchange, + getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; nock.disableNetConnect(); @@ -989,6 +990,55 @@ describe('AwsClient', () => { }); scope.done(); }); + + it('should set x-goog-api-client header correctly', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: expectedSubjectTokenNoToken, + subject_token_type: + 'urn:ietf:params:aws:token-type:aws4_request', + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'aws', + false, + false + ), + } + ) + ); + scopes.push( + nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + ); + process.env.AWS_ACCESS_KEY_ID = accessKeyId; + process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; + + const client = new AwsClient(awsOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); }); }); }); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index f409ad58..4aab6ec3 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -37,7 +37,9 @@ import { mockCloudResourceManager, mockGenerateAccessToken, mockStsTokenExchange, + getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; +import {RefreshOptions} from '../src'; nock.disableNetConnect(); @@ -50,6 +52,14 @@ interface SampleResponse { class TestExternalAccountClient extends BaseExternalAccountClient { private counter = 0; + constructor( + options: BaseExternalAccountClientOptions, + additionalOptions?: RefreshOptions + ) { + super(options, additionalOptions); + this.credentialSourceType = 'test'; + } + async retrieveSubjectToken(): Promise { // Increment subject_token counter each time this is called. return `subject_token_${this.counter++}`; @@ -1020,6 +1030,44 @@ describe('BaseExternalAccountClient', () => { }); scope.done(); }); + + it('should send the correct x-goog-api-client header', async () => { + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'test', + false, + false + ), + } + ); + + const client = new TestExternalAccountClient(externalAccountOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); }); describe('with service account impersonation', () => { @@ -1567,6 +1615,118 @@ describe('BaseExternalAccountClient', () => { }); scopes.forEach(scope => scope.done()); }); + + it('should send correct x-goog-api-client header', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'test', + true, + false + ), + } + ) + ); + scopes.push( + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSA + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should set correct x-goog-api-client header for custom token lifetime', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'test', + true, + true + ), + } + ) + ); + scopes.push( + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + lifetime: 2800, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) + ); + + const externalAccountOptionsWithSATokenLifespan = Object.assign( + { + service_account_impersonation: { + token_lifetime_seconds: 2800, + }, + }, + externalAccountOptionsWithSA + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSATokenLifespan + ); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); }); }); diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index c7383757..1faa5bdd 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -30,6 +30,7 @@ import { getServiceAccountImpersonationUrl, mockGenerateAccessToken, mockStsTokenExchange, + getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; nock.disableNetConnect(); @@ -201,9 +202,9 @@ describe('IdentityPoolClient', () => { 'credentials.' ); - it('should throw when invalid options are provided', () => { + it('should throw when neither file or url sources are provided', () => { const expectedError = new Error( - 'No valid Identity Pool "credential_source" provided' + 'No valid Identity Pool "credential_source" provided, must be either file or url.' ); const invalidOptions = { type: 'external_account', @@ -221,6 +222,27 @@ describe('IdentityPoolClient', () => { }, expectedError); }); + it('should throw when both file and url options are provided', () => { + const expectedError = new Error( + 'No valid Identity Pool "credential_source" provided, must be either file or url.' + ); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: { + file: 'filesource', + url: 'urlsource.com', + }, + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new IdentityPoolClient(invalidOptions as any); + }, expectedError); + }); + it('should throw on invalid credential_source.format.type', () => { const expectedError = new Error('Invalid credential_source format "xml"'); const invalidOptions = { @@ -729,6 +751,45 @@ describe('IdentityPoolClient', () => { ) ); }); + + it('should send the correct x-goog-api-client header value', async () => { + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: fileSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'file', + false, + false + ), + } + ); + + const client = new IdentityPoolClient(fileSourcedOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); }); }); @@ -1037,6 +1098,56 @@ describe('IdentityPoolClient', () => { }); scope.done(); }); + + it('should send the correct x-goog-api-client header value', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token retrieved from url should be used. + subject_token: externalSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'url', + false, + false + ), + } + ) + ); + scopes.push( + nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) + .reply(200, externalSubjectToken) + ); + + const client = new IdentityPoolClient(urlSourcedOptions); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); }); }); }); diff --git a/test/test.pluggableauthclient.ts b/test/test.pluggableauthclient.ts index aea51776..17ecf351 100644 --- a/test/test.pluggableauthclient.ts +++ b/test/test.pluggableauthclient.ts @@ -17,11 +17,14 @@ import { ExecutableError, PluggableAuthClient, } from '../src/auth/pluggable-auth-client'; -import {BaseExternalAccountClient} from '../src'; +import {BaseExternalAccountClient, IdentityPoolClient} from '../src'; import { + assertGaxiosResponsePresent, getAudience, + getExpectedExternalAccountMetricsHeaderValue, getServiceAccountImpersonationUrl, getTokenUrl, + mockStsTokenExchange, saEmail, } from './externalclienthelper'; import {beforeEach} from 'mocha'; @@ -32,6 +35,7 @@ import { InvalidExpirationTimeFieldError, } from '../src/auth/executable-response'; import {PluggableAuthHandler} from '../src/auth/pluggable-auth-handler'; +import {StsSuccessfulResponse} from '../src/auth/stscredentials'; const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token'; const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2'; @@ -92,6 +96,50 @@ describe('PluggableAuthClient', () => { credential_source: pluggableAuthCredentialSourceNoTimeout, }; + const sandbox = sinon.createSandbox(); + let clock: sinon.SinonFakeTimers; + const referenceTime = Date.now(); + let responseJson: ExecutableResponseJson; + let fileStub: sinon.SinonStub<[], Promise>; + let executableStub: sinon.SinonStub< + [envMap: Map], + Promise + >; + + beforeEach(() => { + // Set Allow Executables environment variables to 1 + const envVars = Object.assign({}, process.env, { + GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES: '1', + }); + sandbox.stub(process, 'env').value(envVars); + clock = sinon.useFakeTimers({now: referenceTime}); + + responseJson = { + success: true, + version: 1, + token_type: SAML_SUBJECT_TOKEN_TYPE, + saml_response: 'response', + expiration_time: referenceTime / 1000 + 10, + } as ExecutableResponseJson; + + fileStub = sandbox.stub( + PluggableAuthHandler.prototype, + 'retrieveCachedResponse' + ); + + executableStub = sandbox.stub( + PluggableAuthHandler.prototype, + 'retrieveResponseFromExecutable' + ); + }); + + afterEach(() => { + sandbox.restore(); + if (clock) { + clock.restore(); + } + }); + it('should be a subclass of ExternalAccountClient', () => { assert(PluggableAuthClient.prototype instanceof BaseExternalAccountClient); }); @@ -198,50 +246,6 @@ describe('PluggableAuthClient', () => { }); describe('RetrieveSubjectToken', () => { - const sandbox = sinon.createSandbox(); - let clock: sinon.SinonFakeTimers; - const referenceTime = Date.now(); - let responseJson: ExecutableResponseJson; - let fileStub: sinon.SinonStub<[], Promise>; - let executableStub: sinon.SinonStub< - [envMap: Map], - Promise - >; - - beforeEach(() => { - // Set Allow Executables environment variables to 1 - const envVars = Object.assign({}, process.env, { - GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES: '1', - }); - sandbox.stub(process, 'env').value(envVars); - clock = sinon.useFakeTimers({now: referenceTime}); - - responseJson = { - success: true, - version: 1, - token_type: SAML_SUBJECT_TOKEN_TYPE, - saml_response: 'response', - expiration_time: referenceTime / 1000 + 10, - } as ExecutableResponseJson; - - fileStub = sandbox.stub( - PluggableAuthHandler.prototype, - 'retrieveCachedResponse' - ); - - executableStub = sandbox.stub( - PluggableAuthHandler.prototype, - 'retrieveResponseFromExecutable' - ); - }); - - afterEach(() => { - sandbox.restore(); - if (clock) { - clock.restore(); - } - }); - it('should throw when allow executables environment variables is not 1', async () => { process.env.GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES = '0'; const expectedError = new Error( @@ -476,4 +480,59 @@ describe('PluggableAuthClient', () => { sinon.assert.calledOnceWithExactly(executableStub, expectedEnvMap); }); }); + + describe('GetAccessToken', () => { + const stsSuccessfulResponse: StsSuccessfulResponse = { + access_token: 'ACCESS_TOKEN', + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: 3600, + scope: 'scope1 scope2', + }; + + it('should set x-goog-api-client header correctly', async () => { + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token loaded from file should be used. + subject_token: 'subject_token', + subject_token_type: OIDC_SUBJECT_TOKEN_TYPE1, + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'executable', + false, + false + ), + } + ); + + const client = new PluggableAuthClient(pluggableAuthOptionsOIDC); + responseJson.id_token = 'subject_token'; + responseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; + responseJson.saml_response = undefined; + fileStub.resolves(undefined); + executableStub.resolves(new ExecutableResponse(responseJson)); + + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scope.done(); + }); + }); }); From 1873fefcdf4df00ff40b0f311e40dcdd402f799e Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 28 Jul 2023 23:01:39 +0200 Subject: [PATCH 444/662] fix(deps): update dependency google-auth-library to v9 (#1618) --- samples/puppeteer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 565b8d35..e4c93e45 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -11,7 +11,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^8.0.0", + "google-auth-library": "^9.0.0", "puppeteer": "^20.0.0" } } From 03c0ac9950217ec08ac9ad66eaaa4e9a9e67d423 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 28 Jul 2023 23:30:13 +0200 Subject: [PATCH 445/662] fix(deps): update dependency @googleapis/iam to v11 (#1622) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [@googleapis/iam](https://togithub.com/googleapis/google-api-nodejs-client) | [`^10.0.0` -> `^11.0.0`](https://renovatebot.com/diffs/npm/@googleapis%2fiam/10.0.0/11.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@googleapis%2fiam/11.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@googleapis%2fiam/11.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@googleapis%2fiam/10.0.0/11.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@googleapis%2fiam/10.0.0/11.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/google-api-nodejs-client (@​googleapis/iam) ### [`v11.0.0`](https://togithub.com/googleapis/google-api-nodejs-client/blob/HEAD/CHANGELOG.md#11100-2023-02-10) [Compare Source](https://togithub.com/googleapis/google-api-nodejs-client/compare/10.0.0...11.0.0) ##### ⚠ BREAKING CHANGES - **privateca:** This release has breaking changes. - **metastore:** This release has breaking changes. - **documentai:** This release has breaking changes. - **contentwarehouse:** This release has breaking changes. - **workstations:** This release has breaking changes. - **vmmigration:** This release has breaking changes. - **safebrowsing:** This release has breaking changes. - **integrations:** This release has breaking changes. - **documentai:** This release has breaking changes. - **dataflow:** This release has breaking changes. - **contentwarehouse:** This release has breaking changes. - **cloudtasks:** This release has breaking changes. - **cloudsearch:** This release has breaking changes. - **cloudbuild:** This release has breaking changes. - **chromemanagement:** This release has breaking changes. - **chat:** This release has breaking changes. - **baremetalsolution:** This release has breaking changes. ##### Features - **admin:** update the API ([374bc14](https://togithub.com/googleapis/google-api-nodejs-client/commit/374bc148645a8723c25b8ecdc1cfdfd66451bf03)) - **admob:** update the API ([fcf0cc7](https://togithub.com/googleapis/google-api-nodejs-client/commit/fcf0cc7ee5c74b9952692020546b7b2a651e1607)) - **analyticsadmin:** update the API ([d18d52e](https://togithub.com/googleapis/google-api-nodejs-client/commit/d18d52e3b006a2090712e736a329ae7e9f0ae7ea)) - **analyticsadmin:** update the API ([88d7021](https://togithub.com/googleapis/google-api-nodejs-client/commit/88d702178561a9e35894eab5d667eb81f5b747ba)) - **analyticshub:** update the API ([a9ca643](https://togithub.com/googleapis/google-api-nodejs-client/commit/a9ca6439d720362745b6e540c59b1234a4eb844c)) - **androidenterprise:** update the API ([bd0d5e5](https://togithub.com/googleapis/google-api-nodejs-client/commit/bd0d5e5a0f31a55acf4bb013918fe2091bd37f8e)) - **androidpublisher:** update the API ([02fa6cc](https://togithub.com/googleapis/google-api-nodejs-client/commit/02fa6ccdfe6b992200436584b33ea83861cb3461)) - **apigeeregistry:** update the API ([3d95738](https://togithub.com/googleapis/google-api-nodejs-client/commit/3d95738167afc392d829c403f882bf16ef979c59)) - **apigeeregistry:** update the API ([42d51d0](https://togithub.com/googleapis/google-api-nodejs-client/commit/42d51d0191e0e802f69e0f311dc74df3eef487aa)) - **appengine:** update the API ([cfde2c1](https://togithub.com/googleapis/google-api-nodejs-client/commit/cfde2c112014b6e6d093d98500be5f6025623635)) - **artifactregistry:** update the API ([79b05b9](https://togithub.com/googleapis/google-api-nodejs-client/commit/79b05b9d467d333ea95dbafe053fdddde0e9d8fb)) - **baremetalsolution:** update the API ([877f002](https://togithub.com/googleapis/google-api-nodejs-client/commit/877f00273898bda8a729d09c252f2c7139cd2d95)) - **batch:** update the API ([439160e](https://togithub.com/googleapis/google-api-nodejs-client/commit/439160ef83260c592849325479dac322a4bdabdc)) - **batch:** update the API ([220be1b](https://togithub.com/googleapis/google-api-nodejs-client/commit/220be1b8b5c51c6d323efc1e747d0dfd94f2c168)) - **bigqueryreservation:** update the API ([da46f3f](https://togithub.com/googleapis/google-api-nodejs-client/commit/da46f3f0f12c975caa1b2bc9df2585a31eb0ddd3)) - **bigquery:** update the API ([ad06a50](https://togithub.com/googleapis/google-api-nodejs-client/commit/ad06a50eb9625ea9682b7bbeef0378bd6dc91352)) - **chat:** update the API ([c1f8549](https://togithub.com/googleapis/google-api-nodejs-client/commit/c1f8549520e98e947d09270ce7b2ef298e21aeda)) - **chromemanagement:** update the API ([3368e8d](https://togithub.com/googleapis/google-api-nodejs-client/commit/3368e8dd0e6b92d05fdaf43069758d2c0c01a3f6)) - **chromemanagement:** update the API ([76eea6c](https://togithub.com/googleapis/google-api-nodejs-client/commit/76eea6cdcb93e4c67e70573e729332dc6bff619d)) - **chromepolicy:** update the API ([9c8277a](https://togithub.com/googleapis/google-api-nodejs-client/commit/9c8277a195df18b23e62464f3fdd6b4819b93d52)) - **chromeuxreport:** update the API ([40f2c0d](https://togithub.com/googleapis/google-api-nodejs-client/commit/40f2c0d885a39ba10e94f3f9e117d95ce28cc503)) - **cloudbilling:** update the API ([75ebaa2](https://togithub.com/googleapis/google-api-nodejs-client/commit/75ebaa2ffd27c1ef640474cc9d3b76348f5e67c1)) - **cloudbuild:** update the API ([a264e2d](https://togithub.com/googleapis/google-api-nodejs-client/commit/a264e2d63f2d706e49f92b60cf998aa6a76692e1)) - **clouddeploy:** update the API ([1856ed8](https://togithub.com/googleapis/google-api-nodejs-client/commit/1856ed898d4193f6b24cd1ec18981c7a2575510b)) - **cloudidentity:** update the API ([2b9fb69](https://togithub.com/googleapis/google-api-nodejs-client/commit/2b9fb694cd568421bf64f0e16495c39b9ae7f244)) - **cloudsearch:** update the API ([e743154](https://togithub.com/googleapis/google-api-nodejs-client/commit/e7431549a8c85e8bc5b904cf1241e86716317adf)) - **cloudsearch:** update the API ([ebe2a2b](https://togithub.com/googleapis/google-api-nodejs-client/commit/ebe2a2bf9ed89ff05ebc9e962f34518f21fd6880)) - **cloudtasks:** update the API ([6c5644c](https://togithub.com/googleapis/google-api-nodejs-client/commit/6c5644cee8b07f03217b0d7c300c066ac257df5c)) - **compute:** update the API ([3ef8cb9](https://togithub.com/googleapis/google-api-nodejs-client/commit/3ef8cb962e3ed7201064a1668519094c55c05de1)) - **connectors:** update the API ([7566473](https://togithub.com/googleapis/google-api-nodejs-client/commit/7566473374f5577be7cda803a68feff24c93bc17)) - **contactcenterinsights:** update the API ([2c79661](https://togithub.com/googleapis/google-api-nodejs-client/commit/2c796612bdbc81b7f65978da09fe751e3e536c1f)) - **container:** update the API ([1d0f856](https://togithub.com/googleapis/google-api-nodejs-client/commit/1d0f8569731a8c44ac27eb336966538bbd8a371b)) - **content:** update the API ([8cf1e31](https://togithub.com/googleapis/google-api-nodejs-client/commit/8cf1e316f485453900e54347190353738f4a691c)) - **contentwarehouse:** update the API ([6e89a88](https://togithub.com/googleapis/google-api-nodejs-client/commit/6e89a883c20eb34da508f01f07fd7883c8e1d429)) - **contentwarehouse:** update the API ([cdc217d](https://togithub.com/googleapis/google-api-nodejs-client/commit/cdc217d2d2df7e06dce100dec582ff698aa052ab)) - **datacatalog:** update the API ([76007c1](https://togithub.com/googleapis/google-api-nodejs-client/commit/76007c1eea1486ec6bf62d3cd780e2b512146bf1)) - **datacatalog:** update the API ([f2888f8](https://togithub.com/googleapis/google-api-nodejs-client/commit/f2888f84f15c54e9eb47bcdb78fb2edf85e997c1)) - **dataflow:** update the API ([ad81349](https://togithub.com/googleapis/google-api-nodejs-client/commit/ad81349f15ca8acf94c5654d00737303f712cec7)) - **dataform:** update the API ([613aa91](https://togithub.com/googleapis/google-api-nodejs-client/commit/613aa91900d50a83dece350b3ac54d0852f0e1c6)) - **datamigration:** update the API ([4970228](https://togithub.com/googleapis/google-api-nodejs-client/commit/4970228f449b2dba1db3c0844f1de61c05c296d2)) - **dataplex:** update the API ([f984c3b](https://togithub.com/googleapis/google-api-nodejs-client/commit/f984c3b687e3abe460caa2cb0c3a49b35985ea0c)) - **dataproc:** update the API ([2ff0b3c](https://togithub.com/googleapis/google-api-nodejs-client/commit/2ff0b3c76ecd82ee44cda65d1abf489d4bcb7944)) - **datastore:** update the API ([69b9f8c](https://togithub.com/googleapis/google-api-nodejs-client/commit/69b9f8c82a3daa0cf59d9c87c1a6713b8c6684ea)) - **dialogflow:** update the API ([94e663c](https://togithub.com/googleapis/google-api-nodejs-client/commit/94e663c1de1c324dcdd2f6c5d2cd8bc1b2a69228)) - **discoveryengine:** update the API ([f780df2](https://togithub.com/googleapis/google-api-nodejs-client/commit/f780df29e49db905de631469365aa88c1213e46e)) - **displayvideo:** update the API ([be41c17](https://togithub.com/googleapis/google-api-nodejs-client/commit/be41c177031ca55b1cee79a4385da32e190c7c5a)) - **documentai:** update the API ([24616b9](https://togithub.com/googleapis/google-api-nodejs-client/commit/24616b9d600c32a3d870d6af133600db317e1563)) - **documentai:** update the API ([e213c75](https://togithub.com/googleapis/google-api-nodejs-client/commit/e213c75836f01d5e81058564a0ae864dff6c9eb6)) - **file:** update the API ([3b7046b](https://togithub.com/googleapis/google-api-nodejs-client/commit/3b7046b33fdc0035cca4123c48163a5a4e4e348c)) - **firebasehosting:** update the API ([cd2ec9e](https://togithub.com/googleapis/google-api-nodejs-client/commit/cd2ec9e36306492e10bd4812e402d18c303c5e20)) - **firebase:** update the API ([873a7d7](https://togithub.com/googleapis/google-api-nodejs-client/commit/873a7d7a73813a9405418016f054ea39909c62f9)) - **firebase:** update the API ([e1a3797](https://togithub.com/googleapis/google-api-nodejs-client/commit/e1a3797639ac080d45b762c047a917f41e88d91f)) - **firestore:** update the API ([d9f11cf](https://togithub.com/googleapis/google-api-nodejs-client/commit/d9f11cfe35da7aeca8e790b4266f95804aceeb31)) - **gkehub:** update the API ([9e82b37](https://togithub.com/googleapis/google-api-nodejs-client/commit/9e82b37fbc54357ba44749e3a6a2eb52d1b9e8d0)) - **gkehub:** update the API ([53f583e](https://togithub.com/googleapis/google-api-nodejs-client/commit/53f583ea6423d72e303e644795a3a91064c27345)) - **iam:** update the API ([b149229](https://togithub.com/googleapis/google-api-nodejs-client/commit/b1492297140434849e31c1c0a1e947c61fac7d4a)) - **identitytoolkit:** update the API ([72c533b](https://togithub.com/googleapis/google-api-nodejs-client/commit/72c533b9c6376111cf2d1d96ea1c35c95b629a8a)) - **integrations:** update the API ([7607c9f](https://togithub.com/googleapis/google-api-nodejs-client/commit/7607c9f0a79a0a199bf23ee9231b1220aa5408a0)) - **managedidentities:** update the API ([1a462c2](https://togithub.com/googleapis/google-api-nodejs-client/commit/1a462c246efd76c46efbacc96437a59d11c57331)) - **manufacturers:** update the API ([a927411](https://togithub.com/googleapis/google-api-nodejs-client/commit/a92741128793659316ccb089abe68f26b21d8618)) - **memcache:** update the API ([aa626fc](https://togithub.com/googleapis/google-api-nodejs-client/commit/aa626fc02f4747fe3bea35e63e996694fecfe3a6)) - **metastore:** update the API ([9c7891e](https://togithub.com/googleapis/google-api-nodejs-client/commit/9c7891eaad1176aed5851b4141d319738e587901)) - **metastore:** update the API ([7be6208](https://togithub.com/googleapis/google-api-nodejs-client/commit/7be62088ffb44dd3fe9a89df54a394cd888fcd3e)) - **monitoring:** update the API ([98e55c0](https://togithub.com/googleapis/google-api-nodejs-client/commit/98e55c08500949aef54fc8391d6e1a6201d2ab34)) - **mybusinessverifications:** update the API ([98381ce](https://togithub.com/googleapis/google-api-nodejs-client/commit/98381ce862a15e8fddfa34713d2ba8dd4b3a6cb4)) - **notebooks:** update the API ([9daace9](https://togithub.com/googleapis/google-api-nodejs-client/commit/9daace9906c8c511d8881e56b5660862ff8e1b37)) - **orgpolicy:** update the API ([02f050b](https://togithub.com/googleapis/google-api-nodejs-client/commit/02f050bab5b53e5d526f0446ad83edc2cab52ea6)) - **playdeveloperreporting:** update the API ([9085b75](https://togithub.com/googleapis/google-api-nodejs-client/commit/9085b75885365123991f9ff6b11164343555555a)) - **playintegrity:** update the API ([ab8a18a](https://togithub.com/googleapis/google-api-nodejs-client/commit/ab8a18ae22d36d9b7e6af212626b8c52e7d29c93)) - **privateca:** update the API ([06a61a7](https://togithub.com/googleapis/google-api-nodejs-client/commit/06a61a797bd4b72a4ccc11ca9b5c5fb2e97d8886)) - **prod_tt_sasportal:** update the API ([d3c834b](https://togithub.com/googleapis/google-api-nodejs-client/commit/d3c834b8e744af3da352acf667ec7cec45f41a9b)) - **pubsub:** update the API ([f102db0](https://togithub.com/googleapis/google-api-nodejs-client/commit/f102db0cb8a66035d41251596951d74655984626)) - **recaptchaenterprise:** update the API ([a42a0c8](https://togithub.com/googleapis/google-api-nodejs-client/commit/a42a0c8903c03e381fead675dc0e8c9d3b500899)) - **recommender:** update the API ([88f10a5](https://togithub.com/googleapis/google-api-nodejs-client/commit/88f10a5ceafb499d32690def0d10c54424251376)) - regenerate index files ([93fc048](https://togithub.com/googleapis/google-api-nodejs-client/commit/93fc048d70873141f02ebb3ebb6b86c3d273a887)) - regenerate index files ([31defea](https://togithub.com/googleapis/google-api-nodejs-client/commit/31defea1dca2e32d2cfcea1649087a206e7254d1)) - **retail:** update the API ([3374c58](https://togithub.com/googleapis/google-api-nodejs-client/commit/3374c582eb0528a0b06de867182cd6afa01e1d8e)) - **retail:** update the API ([bb06d0c](https://togithub.com/googleapis/google-api-nodejs-client/commit/bb06d0c6d5bfbc8fa26c7bb4944f62222b79880a)) - **run:** update the API ([8de6284](https://togithub.com/googleapis/google-api-nodejs-client/commit/8de62843e7da5b67dd314c0baa921a209ca15e30)) - **safebrowsing:** update the API ([ecb8989](https://togithub.com/googleapis/google-api-nodejs-client/commit/ecb8989ba492449389bce301095cdc6f76369061)) - **sasportal:** update the API ([42804f6](https://togithub.com/googleapis/google-api-nodejs-client/commit/42804f6fc3ff4a4d55508f193d7ecfc7c73ae53a)) - **servicecontrol:** update the API ([2af2ce1](https://togithub.com/googleapis/google-api-nodejs-client/commit/2af2ce1622307ab2f59970399f5a95193466edc5)) - **servicenetworking:** update the API ([d632167](https://togithub.com/googleapis/google-api-nodejs-client/commit/d632167eaff51ac9f8a8c98a321743b52e5d90d0)) - **streetviewpublish:** update the API ([c9f2655](https://togithub.com/googleapis/google-api-nodejs-client/commit/c9f26558424ec52a67df4825a649ab2d4975cdcb)) - **sts:** update the API ([b6e069b](https://togithub.com/googleapis/google-api-nodejs-client/commit/b6e069bd1a5618685f949d97606feeb0204aa8fb)) - **testing:** update the API ([f7aa8c8](https://togithub.com/googleapis/google-api-nodejs-client/commit/f7aa8c877911e020068e6347568392aff48b9403)) - **texttospeech:** update the API ([c772fc1](https://togithub.com/googleapis/google-api-nodejs-client/commit/c772fc10a477730d0dd4c93a4aca33fc1cea8bd9)) - **tpu:** update the API ([b92d702](https://togithub.com/googleapis/google-api-nodejs-client/commit/b92d702d5a448c1026bbd16dc33223d667cca71f)) - **translate:** update the API ([07babc3](https://togithub.com/googleapis/google-api-nodejs-client/commit/07babc3495a644c37dc8a42ded56c8190d07e3f7)) - **vmmigration:** update the API ([99b557a](https://togithub.com/googleapis/google-api-nodejs-client/commit/99b557a73a2affe5e2ad7bfd454322100abe0168)) - **workflowexecutions:** update the API ([765ac58](https://togithub.com/googleapis/google-api-nodejs-client/commit/765ac5895df40a970ab0e7ec9299c94713e230cc)) - **workflows:** update the API ([1ed85b8](https://togithub.com/googleapis/google-api-nodejs-client/commit/1ed85b8ad280ac61883f72948e94bc3b5f90b56f)) - **workstations:** update the API ([84b748e](https://togithub.com/googleapis/google-api-nodejs-client/commit/84b748e71d395e8652450252fc6051bb16659c8a)) - **workstations:** update the API ([cbe98ce](https://togithub.com/googleapis/google-api-nodejs-client/commit/cbe98ce923d06e03d5fcc39798bec44ade43681c)) ##### Bug Fixes - **accessapproval:** update the API ([bfbd589](https://togithub.com/googleapis/google-api-nodejs-client/commit/bfbd5898dfda3f92e20a04f7987282956f532b6b)) - **accesscontextmanager:** update the API ([f3cc068](https://togithub.com/googleapis/google-api-nodejs-client/commit/f3cc068cd73cb916739493c211b3cfb65eaff109)) - **advisorynotifications:** update the API ([7021d81](https://togithub.com/googleapis/google-api-nodejs-client/commit/7021d81f265416bca23210d909949cc668fc7f88)) - **alertcenter:** update the API ([e18c3b9](https://togithub.com/googleapis/google-api-nodejs-client/commit/e18c3b974e13652c3ad9845312dd384c7a369ec6)) - **androidmanagement:** update the API ([080e682](https://togithub.com/googleapis/google-api-nodejs-client/commit/080e682796aca945201ccf975dfce69457f58da5)) - **androidpublisher:** update the API ([5c62a79](https://togithub.com/googleapis/google-api-nodejs-client/commit/5c62a795376acc707b791ed2f7359ccb34f8512c)) - **apigateway:** update the API ([df258eb](https://togithub.com/googleapis/google-api-nodejs-client/commit/df258ebef47ba111fe49b657766faf06843798ef)) - **assuredworkloads:** update the API ([98dea74](https://togithub.com/googleapis/google-api-nodejs-client/commit/98dea740039110bf1fd1c2d91f52f5bf9524e37d)) - **bigqueryconnection:** update the API ([93ef743](https://togithub.com/googleapis/google-api-nodejs-client/commit/93ef743e4bdb17164cee722f1514342aeedd756e)) - **bigqueryreservation:** update the API ([b41049e](https://togithub.com/googleapis/google-api-nodejs-client/commit/b41049e183240e842ead905c74e6546809a8c14d)) - **bigquery:** update the API ([fd7c15c](https://togithub.com/googleapis/google-api-nodejs-client/commit/fd7c15ccf33957ae50ce5f36792107817f35e2b2)) - **bigtableadmin:** update the API ([3dc3bf2](https://togithub.com/googleapis/google-api-nodejs-client/commit/3dc3bf245b7ed6b35964d4962c5352273cd164e1)) - **bigtableadmin:** update the API ([4681456](https://togithub.com/googleapis/google-api-nodejs-client/commit/46814562cd85463c078b19f536c277ffc31c89d2)) - **binaryauthorization:** update the API ([469c0d2](https://togithub.com/googleapis/google-api-nodejs-client/commit/469c0d2446c2339533a3ce7fd5608bb1bfa6d5e6)) - **chat:** update the API ([7e53df7](https://togithub.com/googleapis/google-api-nodejs-client/commit/7e53df78fd2839237dfa28ffbc1a7e0326efb293)) - **chromeuxreport:** update the API ([4b307dc](https://togithub.com/googleapis/google-api-nodejs-client/commit/4b307dc7bd9855bee38b2c6f365a5a7f7ae792c5)) - **classroom:** update the API ([55e7f4b](https://togithub.com/googleapis/google-api-nodejs-client/commit/55e7f4bb47fa2fb5da46947ff108204b63aa4fc8)) - **cloudasset:** update the API ([ed6c311](https://togithub.com/googleapis/google-api-nodejs-client/commit/ed6c311f52e6191e935825e1b245b498326c9008)) - **cloudasset:** update the API ([538cbab](https://togithub.com/googleapis/google-api-nodejs-client/commit/538cbab358f74d0df49cf0f28c7de8243ae0f8c0)) - **cloudchannel:** update the API ([e4b1b2f](https://togithub.com/googleapis/google-api-nodejs-client/commit/e4b1b2fd8d0825b4423458dc7beccfd3ca74e2fa)) - **cloudfunctions:** update the API ([3e8cd4c](https://togithub.com/googleapis/google-api-nodejs-client/commit/3e8cd4c8321f5139850515ed661dc88b74db05c8)) - **cloudiot:** update the API ([3e7b5ff](https://togithub.com/googleapis/google-api-nodejs-client/commit/3e7b5ff7e5ee50011d607c54e06976de21f23f21)) - **cloudkms:** update the API ([2df4942](https://togithub.com/googleapis/google-api-nodejs-client/commit/2df494233472bd332086875662e5bf3e3b70c501)) - **cloudresourcemanager:** update the API ([e49d213](https://togithub.com/googleapis/google-api-nodejs-client/commit/e49d213024c2d6dc14ff06d0e332f9fac7d7a378)) - **cloudsupport:** update the API ([9d69ef4](https://togithub.com/googleapis/google-api-nodejs-client/commit/9d69ef45bb22779a86ad12d9843b21fcf3c6752c)) - **cloudtasks:** update the API ([083c4d1](https://togithub.com/googleapis/google-api-nodejs-client/commit/083c4d107e09e0de6c16c75a95d8a45832d26b23)) - **cloudtrace:** update the API ([07342c6](https://togithub.com/googleapis/google-api-nodejs-client/commit/07342c6cd7fb9d5e32d99f3a48b2112fee7c133b)) - **cloudtrace:** update the API ([658ac0d](https://togithub.com/googleapis/google-api-nodejs-client/commit/658ac0daa112211ce0327ed958135110e4eceb12)) - **composer:** update the API ([2e95e28](https://togithub.com/googleapis/google-api-nodejs-client/commit/2e95e28461a1986187519b25f56a832a225afe03)) - **connectors:** update the API ([ad16263](https://togithub.com/googleapis/google-api-nodejs-client/commit/ad1626335affcd2a71bc544231c9e65ee9c4e7ee)) - **contactcenteraiplatform:** update the API ([5cf0bbd](https://togithub.com/googleapis/google-api-nodejs-client/commit/5cf0bbdda0be63a07785bc5f5457ba5ba275f180)) - **containeranalysis:** update the API ([baccc47](https://togithub.com/googleapis/google-api-nodejs-client/commit/baccc476bcadbcb471c046f641bf486a122ba9ac)) - **containeranalysis:** update the API ([43992e0](https://togithub.com/googleapis/google-api-nodejs-client/commit/43992e01a6146e8016df770f4f6fceead9a3f00e)) - **content:** update the API ([6b0614b](https://togithub.com/googleapis/google-api-nodejs-client/commit/6b0614bc80883bdf9bd8146c90566264b7bd3808)) - **dataproc:** update the API ([19c4726](https://togithub.com/googleapis/google-api-nodejs-client/commit/19c47266279c74291cff0d618c7d5f933a72c9ef)) - **dlp:** update the API ([8b96a97](https://togithub.com/googleapis/google-api-nodejs-client/commit/8b96a97a03e5899888980c1e5c5a59e18de387ae)) - **dns:** update the API ([60542d9](https://togithub.com/googleapis/google-api-nodejs-client/commit/60542d9c18cc5ae9df2264d41cad594ce1d4f59d)) - **domains:** update the API ([5f33f78](https://togithub.com/googleapis/google-api-nodejs-client/commit/5f33f78cad39250c7dd47c5e75c8429f8d737347)) - **driveactivity:** update the API ([2858e5a](https://togithub.com/googleapis/google-api-nodejs-client/commit/2858e5ae7cbfd159230e060de0034221a418ac16)) - **drive:** update the API ([885131d](https://togithub.com/googleapis/google-api-nodejs-client/commit/885131d5a20cd4b6106d685ee312f6e128e346bb)) - **eventarc:** update the API ([116f6ff](https://togithub.com/googleapis/google-api-nodejs-client/commit/116f6ffb3736fadba15054507185e99f43247e4f)) - **eventarc:** update the API ([8f35984](https://togithub.com/googleapis/google-api-nodejs-client/commit/8f359849bac88bbaa7c079537b9d8d2b17c4b8fe)) - **firebasehosting:** update the API ([072eac4](https://togithub.com/googleapis/google-api-nodejs-client/commit/072eac4dfb54ac2c52b0206e693b43490a369402)) - **gameservices:** update the API ([8a8cc4b](https://togithub.com/googleapis/google-api-nodejs-client/commit/8a8cc4b88c75324a3a90989d1dc1a2eaca7668e7)) - **gmail:** update the API ([454caaf](https://togithub.com/googleapis/google-api-nodejs-client/commit/454caaf1e282d8afb2dd933ec6cf56c1193a27e5)) - **healthcare:** update the API ([fa9914f](https://togithub.com/googleapis/google-api-nodejs-client/commit/fa9914febbf99733117748d0d404b42318aa5870)) - **homegraph:** update the API ([2b517eb](https://togithub.com/googleapis/google-api-nodejs-client/commit/2b517ebc327ab6f445f5a5594acb2c3bb37bfa8a)) - **iam:** update the API ([092031f](https://togithub.com/googleapis/google-api-nodejs-client/commit/092031f3ccb9a7bb357c0e3701864c634f371f99)) - **iap:** update the API ([01b97e9](https://togithub.com/googleapis/google-api-nodejs-client/commit/01b97e9862874f031dbf4241ff45793925922419)) - **ideahub:** update the API ([0a334ea](https://togithub.com/googleapis/google-api-nodejs-client/commit/0a334ea6717ab691eeda6a697cefeea079d9a2f3)) - **jobs:** update the API ([d26f462](https://togithub.com/googleapis/google-api-nodejs-client/commit/d26f46295622577c63ee8189f4e56801622b9592)) - **kmsinventory:** update the API ([71a38e2](https://togithub.com/googleapis/google-api-nodejs-client/commit/71a38e29ada3c23e319a378f4cb3f8f7e92089dd)) - **logging:** update the API ([d24bca1](https://togithub.com/googleapis/google-api-nodejs-client/commit/d24bca1415f9003aec0af8123d35302f536aad64)) - **memcache:** update the API ([d603fb9](https://togithub.com/googleapis/google-api-nodejs-client/commit/d603fb9811b68f08cafe0e944e0cea0bff9b34b3)) - **ml:** update the API ([2ae0fb5](https://togithub.com/googleapis/google-api-nodejs-client/commit/2ae0fb50e04a8722d793b500d53234aae420a846)) - **monitoring:** update the API ([9da4383](https://togithub.com/googleapis/google-api-nodejs-client/commit/9da4383c839d99d6e438d6e690876f833d974b38)) - **mybusinessaccountmanagement:** update the API ([413722a](https://togithub.com/googleapis/google-api-nodejs-client/commit/413722ab846c0c06bf30481bfb224186cbeb2161)) - **mybusinesslodging:** update the API ([28f185b](https://togithub.com/googleapis/google-api-nodejs-client/commit/28f185b430329233ac7faf9204db1fdf88f3ef1d)) - **networkconnectivity:** update the API ([ed2ccaa](https://togithub.com/googleapis/google-api-nodejs-client/commit/ed2ccaa59644b829295fd1654cee304562d485d0)) - **networksecurity:** update the API ([05ad966](https://togithub.com/googleapis/google-api-nodejs-client/commit/05ad966e92e2cba10c3dfa3a34d8cf5a9c5b0043)) - **networkservices:** update the API ([77405ce](https://togithub.com/googleapis/google-api-nodejs-client/commit/77405ce81969f68ba56c11583d6cd9978500f3f3)) - **orgpolicy:** update the API ([56f36f9](https://togithub.com/googleapis/google-api-nodejs-client/commit/56f36f95e66a31cd1d98ed6c2f86b5daacb38776)) - **osconfig:** update the API ([3e34814](https://togithub.com/googleapis/google-api-nodejs-client/commit/3e348143a65bcdeaa7e2e63d360d8a390bd58947)) - **paymentsresellersubscription:** update the API ([fdb70dc](https://togithub.com/googleapis/google-api-nodejs-client/commit/fdb70dca11d48dec792dc832033379c00192386b)) - **people:** update the API ([fa1e010](https://togithub.com/googleapis/google-api-nodejs-client/commit/fa1e0103a1d4c92c170a485da7619d39cb23374f)) - **policytroubleshooter:** update the API ([68e00d3](https://togithub.com/googleapis/google-api-nodejs-client/commit/68e00d3f06ec1eeddfaf42a7fb0f264108dc9487)) - **pubsub:** update the API ([ae69416](https://togithub.com/googleapis/google-api-nodejs-client/commit/ae694165d2eabf95df862b3f8dd6c6f6e69d92ee)) - **runtimeconfig:** update the API ([5153504](https://togithub.com/googleapis/google-api-nodejs-client/commit/51535042adef603221bb831e3ec5a0e4e626fead)) - **run:** update the API ([27d36c8](https://togithub.com/googleapis/google-api-nodejs-client/commit/27d36c8bf63fad7a898374ced37c7d0321e72dad)) - **searchads360:** update the API ([14426a0](https://togithub.com/googleapis/google-api-nodejs-client/commit/14426a01847942019fdfa56ee26f0df64ed30898)) - **secretmanager:** update the API ([7f66ecb](https://togithub.com/googleapis/google-api-nodejs-client/commit/7f66ecb48728229e37b2e791ad0c307aeaa15d55)) - **securitycenter:** update the API ([8e4017b](https://togithub.com/googleapis/google-api-nodejs-client/commit/8e4017b1742c46885ded5adbebf294b60b9e9f09)) - **servicemanagement:** update the API ([bb1f964](https://togithub.com/googleapis/google-api-nodejs-client/commit/bb1f9647844921d566282cd36d1c7057241d311b)) - **sourcerepo:** update the API ([ba3d1d1](https://togithub.com/googleapis/google-api-nodejs-client/commit/ba3d1d1afb8769d6f1db0c82f7c6eb5424baaf36)) - **speech:** update the API ([f60d970](https://togithub.com/googleapis/google-api-nodejs-client/commit/f60d9708d893d5d10a3ba58911023eebf89e67ac)) - **sts:** update the API ([df55772](https://togithub.com/googleapis/google-api-nodejs-client/commit/df5577227f9833b53c24dc9e03b08553a91f20da)) - **toolresults:** update the API ([656c975](https://togithub.com/googleapis/google-api-nodejs-client/commit/656c9754fde6b7d46238b0873ebe1f1acb7501f3)) - **tpu:** update the API ([fe71ffb](https://togithub.com/googleapis/google-api-nodejs-client/commit/fe71ffb8a42a733efbb872979d3fe4268c1ed5cb)) - **vault:** update the API ([444f5cf](https://togithub.com/googleapis/google-api-nodejs-client/commit/444f5cf5299dd0519c48f4ff756ef8d540db0285)) - **youtube:** update the API ([a275134](https://togithub.com/googleapis/google-api-nodejs-client/commit/a275134286b36e78c9b5336f9b04af4914be82da))
--- ### Configuration 📅 **Schedule**: Branch creation - "after 9am and before 3pm" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/google-auth-library-nodejs). --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index e476f224..5211b3a5 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^6.0.0", - "@googleapis/iam": "^10.0.0", + "@googleapis/iam": "^11.0.0", "google-auth-library": "^9.0.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 01372f3993b0b023006f1f12dfed08a7ec6e6dbe Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 28 Jul 2023 23:40:45 +0200 Subject: [PATCH 446/662] chore(deps): update dependency puppeteer to v20 (#1617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [puppeteer](https://togithub.com/puppeteer/puppeteer/tree/main#readme) ([source](https://togithub.com/puppeteer/puppeteer)) | [`^19.0.0` -> `^20.0.0`](https://renovatebot.com/diffs/npm/puppeteer/19.11.1/20.9.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/puppeteer/20.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/puppeteer/20.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/puppeteer/19.11.1/20.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/puppeteer/19.11.1/20.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
puppeteer/puppeteer (puppeteer) ### [`v20.9.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.9.0): puppeteer: v20.9.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.8.3...puppeteer-v20.9.0) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.8.3 to 20.9.0 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.4.5 to 1.4.6 ### [`v20.8.3`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.8.3): puppeteer: v20.8.3 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.8.2...puppeteer-v20.8.3) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.8.2 to 20.8.3 ### [`v20.8.2`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.8.2): puppeteer: v20.8.2 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.8.1...puppeteer-v20.8.2) ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.8.1 to 20.8.2 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.4.4 to 1.4.5 ### [`v20.8.1`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.8.1): puppeteer: v20.8.1 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.8.0...puppeteer-v20.8.1) ##### Bug Fixes - remove test metadata files ([#​10520](https://togithub.com/puppeteer/puppeteer/issues/10520)) ([cbf4f2a](https://togithub.com/puppeteer/puppeteer/commit/cbf4f2a66912f24849ae8c88fc1423851dcc4aa7)) ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.8.0 to 20.8.1 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.4.3 to 1.4.4 ### [`v20.8.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.8.0): puppeteer: v20.8.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.7.4...puppeteer-v20.8.0) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.7.4 to 20.8.0 ### [`v20.7.4`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.7.4): puppeteer: v20.7.4 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.7.3...puppeteer-v20.7.4) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.7.3 to 20.7.4 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.4.2 to 1.4.3 ### [`v20.7.3`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.7.3): puppeteer: v20.7.3 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.7.2...puppeteer-v20.7.3) ##### Bug Fixes - include src into published package ([#​10415](https://togithub.com/puppeteer/puppeteer/issues/10415)) ([d1ffad0](https://togithub.com/puppeteer/puppeteer/commit/d1ffad059ae66104842b92dc814d362c123b9646)) ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.7.2 to 20.7.3 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.4.1 to 1.4.2 ### [`v20.7.2`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.7.2): puppeteer: v20.7.2 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.7.1...puppeteer-v20.7.2) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.7.1 to 20.7.2 ### [`v20.7.1`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.7.1): puppeteer: v20.7.1 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.7.0...puppeteer-v20.7.1) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.7.0 to 20.7.1 ### [`v20.7.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.7.0): puppeteer: v20.7.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.6.0...puppeteer-v20.7.0) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.6.0 to 20.7.0 ### [`v20.6.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.6.0): puppeteer: v20.6.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.5.0...puppeteer-v20.6.0) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.5.0 to 20.6.0 ### [`v20.5.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.5.0): puppeteer: v20.5.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.4.0...puppeteer-v20.5.0) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.4.0 to 20.5.0 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.4.0 to 1.4.1 ### [`v20.4.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.4.0): puppeteer: v20.4.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.3.0...puppeteer-v20.4.0) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.3.0 to 20.4.0 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.3.0 to 1.4.0 ### [`v20.3.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.3.0): puppeteer: v20.3.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.2.1...puppeteer-v20.3.0) ##### Features - add an ability to trim cache for Puppeteer ([#​10199](https://togithub.com/puppeteer/puppeteer/issues/10199)) ([1ad32ec](https://togithub.com/puppeteer/puppeteer/commit/1ad32ec9948ca3e07e15548a562c8f3c633b3dc3)) ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.2.1 to 20.3.0 ### [`v20.2.1`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.2.1): puppeteer: v20.2.1 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.2.0...puppeteer-v20.2.1) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.2.0 to 20.2.1 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.2.0 to 1.3.0 ### [`v20.2.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.2.0): puppeteer: v20.2.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.1.2...puppeteer-v20.2.0) ##### Bug Fixes - downloadPath should be used by the install script ([#​10163](https://togithub.com/puppeteer/puppeteer/issues/10163)) ([4398f66](https://togithub.com/puppeteer/puppeteer/commit/4398f66f281f1ffe5be81b529fc4751edfaf761d)) ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.1.2 to 20.2.0 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.1.0 to 1.2.0 ### [`v20.1.2`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.1.2): puppeteer: v20.1.2 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.1.1...puppeteer-v20.1.2) ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.1.1 to 20.1.2 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.0.1 to 1.1.0 ### [`v20.1.1`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.1.1): puppeteer: v20.1.1 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.1.0...puppeteer-v20.1.1) ##### Bug Fixes - rename PUPPETEER_DOWNLOAD_HOST to PUPPETEER_DOWNLOAD_BASE_URL ([#​10130](https://togithub.com/puppeteer/puppeteer/issues/10130)) ([9758cae](https://togithub.com/puppeteer/puppeteer/commit/9758cae029f90908c4b5340561d9c51c26aa2f21)) ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.1.0 to 20.1.1 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.0.0 to 1.0.1 ### [`v20.1.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.1.0): puppeteer: v20.1.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.0.0...puppeteer-v20.1.0) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.0.0 to 20.1.0 ### [`v20.0.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v20.0.0): puppeteer: v20.0.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v19.11.1...puppeteer-v20.0.0) ##### ⚠ BREAKING CHANGES - switch to Chrome for Testing instead of Chromium ([#​10054](https://togithub.com/puppeteer/puppeteer/issues/10054)) ##### Features - switch to Chrome for Testing instead of Chromium ([#​10054](https://togithub.com/puppeteer/puppeteer/issues/10054)) ([df4d60c](https://togithub.com/puppeteer/puppeteer/commit/df4d60c187aa11c4ad783827242e9511f4ec2aab)) ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 19.11.1 to 20.0.0 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 0.5.0 to 1.0.0
--- ### Configuration 📅 **Schedule**: Branch creation - "after 9am and before 3pm" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef264553..509efd4f 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^19.0.0", + "puppeteer": "^20.0.0", "sinon": "^15.0.0", "ts-loader": "^8.0.0", "typescript": "^5.1.6", From 8075b6fbb347067ec231134fb4388ab9bcd1bde0 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 8 Aug 2023 00:49:57 +0200 Subject: [PATCH 447/662] fix(deps): update dependency @google-cloud/storage to v7 (#1629) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 5211b3a5..8bd760eb 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@google-cloud/storage": "^6.0.0", + "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^11.0.0", "google-auth-library": "^9.0.0", "node-fetch": "^2.3.0", From b4c8d18def7460875e5eed09b43e2f2b8e77e58a Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 22:56:13 +0000 Subject: [PATCH 448/662] docs: fix node release schedule link (#1628) Co-authored-by: Jeffrey Rennie Source-Link: https://togithub.com/googleapis/synthtool/commit/1a2431537d603e95b4b32317fb494542f75a2165 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:e08f9a3757808cdaf7a377e962308c65c4d7eff12db206d4fae702dd50d43430 --- .github/.OwlBot.lock.yaml | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index de4fa0a5..a3d003c6 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:ef104a520c849ffde60495342ecf099dfb6256eab0fbd173228f447bc73d1aa9 -# created: 2023-07-10T21:36:52.433664553Z + digest: sha256:e08f9a3757808cdaf7a377e962308c65c4d7eff12db206d4fae702dd50d43430 +# created: 2023-08-03T18:46:14.719706948Z diff --git a/README.md b/README.md index 0fef4af0..83314534 100644 --- a/README.md +++ b/README.md @@ -1241,7 +1241,7 @@ also contains samples. ## Supported Node.js Versions -Our client libraries follow the [Node.js release schedule](https://nodejs.org/en/about/releases/). +Our client libraries follow the [Node.js release schedule](https://github.com/nodejs/release#release-schedule). Libraries are compatible with all current _active_ and _maintenance_ versions of Node.js. If you are using an end-of-life version of Node.js, we recommend that you update From dd04fbdeb17e457a09b06a4abc2aedf511f355ea Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 8 Aug 2023 01:04:12 +0200 Subject: [PATCH 449/662] fix(deps): update dependency puppeteer to v21 (#1627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [puppeteer](https://togithub.com/puppeteer/puppeteer/tree/main#readme) ([source](https://togithub.com/puppeteer/puppeteer)) | [`^20.0.0` -> `^21.0.0`](https://renovatebot.com/diffs/npm/puppeteer/20.9.0/21.0.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/puppeteer/21.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/puppeteer/21.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/puppeteer/20.9.0/21.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/puppeteer/20.9.0/21.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
puppeteer/puppeteer (puppeteer) ### [`v21.0.1`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v21.0.1): puppeteer: v21.0.1 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v21.0.0...puppeteer-v21.0.1) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 21.0.0 to 21.0.1 ### [`v21.0.0`](https://togithub.com/puppeteer/puppeteer/releases/tag/puppeteer-v21.0.0): puppeteer: v21.0.0 [Compare Source](https://togithub.com/puppeteer/puppeteer/compare/puppeteer-v20.9.0...puppeteer-v21.0.0) ##### Miscellaneous Chores - **puppeteer:** Synchronize puppeteer versions ##### Dependencies - The following workspace dependencies were updated - dependencies - puppeteer-core bumped from 20.9.0 to 21.0.0 - [@​puppeteer/browsers](https://togithub.com/puppeteer/browsers) bumped from 1.4.6 to 1.5.0
--- ### Configuration 📅 **Schedule**: Branch creation - "after 9am and before 3pm" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/google-auth-library-nodejs). --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 509efd4f..aa9299ea 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^20.0.0", + "puppeteer": "^21.0.0", "sinon": "^15.0.0", "ts-loader": "^8.0.0", "typescript": "^5.1.6", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index e4c93e45..ced88955 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^9.0.0", - "puppeteer": "^20.0.0" + "puppeteer": "^21.0.0" } } From bf23bad915cae8994b2953b406d04112848c5224 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 9 Aug 2023 10:37:19 -0700 Subject: [PATCH 450/662] chore: lint fixes for `gts` 5.0 (#1625) * chore: lint fixes for `gts` 5.0 * chore(deps): update `gts` --- package.json | 2 +- src/auth/iam.ts | 5 ++++- system-test/fixtures/kitchen/package.json | 2 +- test/test.pluggableauthhandler.ts | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index aa9299ea..113c798a 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "chai": "^4.2.0", "codecov": "^3.0.2", "execa": "^5.0.0", - "gts": "^3.1.1", + "gts": "^5.0.0", "is-docker": "^2.0.0", "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", diff --git a/src/auth/iam.ts b/src/auth/iam.ts index 4c38027f..6e789b11 100644 --- a/src/auth/iam.ts +++ b/src/auth/iam.ts @@ -25,7 +25,10 @@ export class IAMAuth { * @param token the token * @constructor */ - constructor(public selector: string, public token: string) { + constructor( + public selector: string, + public token: string + ) { this.selector = selector; this.token = token; } diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index b52a08b2..79308005 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@types/node": "^16.11.3", "typescript": "^3.0.0", - "gts": "^2.0.0", + "gts": "^5.0.0", "null-loader": "^4.0.0", "ts-loader": "^8.0.0", "webpack": "^4.20.2", diff --git a/test/test.pluggableauthhandler.ts b/test/test.pluggableauthhandler.ts index b82710cb..7a375aa8 100644 --- a/test/test.pluggableauthhandler.ts +++ b/test/test.pluggableauthhandler.ts @@ -100,7 +100,7 @@ describe('PluggableAuthHandler', () => { [ command: string, args: readonly string[], - options: child_process.SpawnOptions + options: child_process.SpawnOptions, ], child_process.ChildProcess >; @@ -360,7 +360,7 @@ describe('PluggableAuthHandler', () => { let realPathStub: sinon.SinonStub< [ path: fs.PathLike, - options?: fs.ObjectEncodingOptions | BufferEncoding | null | undefined + options?: fs.ObjectEncodingOptions | BufferEncoding | null | undefined, ], Promise >; From 4ee02da4f6934eeb9550b8b659d1abda85194e6b Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 31 Aug 2023 12:29:50 -0700 Subject: [PATCH 451/662] feat: extend `universe_domain` support (#1633) * feat: extend `universe_domain` support * style: lint * chore: minor fix * chore: lint * chore: lint * run lint --------- Co-authored-by: Sofia Leon --- src/auth/baseexternalclient.ts | 29 +++++++++++++----- .../externalAccountAuthorizedUserClient.ts | 30 +++++++++++-------- test/test.baseexternalclient.ts | 22 +++++++++++++- ...est.externalaccountauthorizeduserclient.ts | 25 +++++++++++++++- 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index f29c2787..8598cd69 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -62,24 +62,36 @@ const WORKFORCE_AUDIENCE_PATTERN = // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); +/** + * The default cloud universe + */ +export const DEFAULT_UNIVERSE = 'googleapis.com'; + +export interface SharedExternalAccountClientOptions { + audience: string; + token_url: string; + quota_project_id?: string; + /** + * universe domain is the default service domain for a given Cloud universe + */ + universe_domain?: string; +} + /** * Base external account credentials json interface. */ -export interface BaseExternalAccountClientOptions { +export interface BaseExternalAccountClientOptions + extends SharedExternalAccountClientOptions { type: string; - audience: string; subject_token_type: string; service_account_impersonation_url?: string; service_account_impersonation?: { token_lifetime_seconds?: number; }; - token_url: string; token_info_url?: string; client_id?: string; client_secret?: string; - quota_project_id?: string; workforce_pool_user_project?: string; - universe_domain?: string; } /** @@ -139,7 +151,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { private readonly stsCredential: sts.StsCredentials; private readonly clientAuth?: ClientAuthentication; private readonly workforcePoolUserProject?: string; - private universeDomain?: string; + public universeDomain = DEFAULT_UNIVERSE; public projectId: string | null; public projectNumber: string | null; public readonly eagerRefreshThresholdMillis: number; @@ -217,7 +229,10 @@ export abstract class BaseExternalAccountClient extends AuthClient { this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; this.projectId = null; this.projectNumber = this.getProjectNumber(this.audience); - this.universeDomain = options.universe_domain; + + if (options.universe_domain) { + this.universeDomain = options.universe_domain; + } } /** The service account email to be impersonated, if available. */ diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index ca267bda..3c05cc56 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -29,29 +29,30 @@ import { } from 'gaxios'; import {Credentials} from './credentials'; import * as stream from 'stream'; -import {EXPIRATION_TIME_OFFSET} from './baseexternalclient'; +import { + DEFAULT_UNIVERSE, + EXPIRATION_TIME_OFFSET, + SharedExternalAccountClientOptions, +} from './baseexternalclient'; + +/** + * The credentials JSON file type for external account authorized user clients. + */ +export const EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE = + 'external_account_authorized_user'; /** * External Account Authorized User Credentials JSON interface. */ -export interface ExternalAccountAuthorizedUserClientOptions { +export interface ExternalAccountAuthorizedUserClientOptions + extends SharedExternalAccountClientOptions { type: typeof EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE; - audience: string; client_id: string; client_secret: string; refresh_token: string; - token_url: string; token_info_url: string; revoke_url?: string; - quota_project_id?: string; } - -/** - * The credentials JSON file type for external account authorized user clients. - */ -export const EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE = - 'external_account_authorized_user'; - /** * Internal interface for tracking the access token expiration time. */ @@ -155,6 +156,7 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { private cachedAccessToken: CredentialsWithResponse | null; private readonly externalAccountAuthorizedUserHandler: ExternalAccountAuthorizedUserHandler; private refreshToken: string; + public universeDomain = DEFAULT_UNIVERSE; /** * Instantiates an ExternalAccountAuthorizedUserClient instances using the @@ -197,6 +199,10 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { .eagerRefreshThresholdMillis as number; } this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; + + if (options.universe_domain) { + this.universeDomain = options.universe_domain; + } } async getAccessToken(): Promise<{ diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 4aab6ec3..826326a8 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -23,6 +23,7 @@ import { EXPIRATION_TIME_OFFSET, BaseExternalAccountClient, BaseExternalAccountClientOptions, + DEFAULT_UNIVERSE, } from '../src/auth/baseexternalclient'; import { OAuthErrorResponse, @@ -80,7 +81,6 @@ describe('BaseExternalAccountClient', () => { credential_source: { file: '/var/run/secrets/goog.id/token', }, - universe_domain: 'universe.domain.com', }; const externalAccountOptionsWithCreds = { type: 'external_account', @@ -294,6 +294,26 @@ describe('BaseExternalAccountClient', () => { }); }); + describe('universeDomain', () => { + it('should be the default universe if not set', () => { + const client = new TestExternalAccountClient(externalAccountOptions); + + assert.equal(client.universeDomain, DEFAULT_UNIVERSE); + }); + + it('should be set if provided', () => { + const universeDomain = 'my-universe.domain.com'; + const options: BaseExternalAccountClientOptions = { + ...externalAccountOptions, + universe_domain: universeDomain, + }; + + const client = new TestExternalAccountClient(options); + + assert.equal(client.universeDomain, universeDomain); + }); + }); + describe('getServiceAccountEmail()', () => { it('should return the service account email when impersonation is used', () => { const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index f9dcc45e..c97e4b03 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -23,7 +23,10 @@ import { ExternalAccountAuthorizedUserClient, ExternalAccountAuthorizedUserClientOptions, } from '../src/auth/externalAccountAuthorizedUserClient'; -import {EXPIRATION_TIME_OFFSET} from '../src/auth/baseexternalclient'; +import { + DEFAULT_UNIVERSE, + EXPIRATION_TIME_OFFSET, +} from '../src/auth/baseexternalclient'; import {GaxiosError, GaxiosResponse} from 'gaxios'; import { getErrorFromOAuthErrorResponse, @@ -149,6 +152,26 @@ describe('ExternalAccountAuthorizedUserClient', () => { refreshOptions.eagerRefreshThresholdMillis ); }); + + describe('universeDomain', () => { + it('should be the default universe if not set', () => { + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions + ); + + assert.equal(client.universeDomain, DEFAULT_UNIVERSE); + }); + + it('should be set if provided', () => { + const universeDomain = 'my-universe.domain.com'; + const client = new ExternalAccountAuthorizedUserClient({ + ...externalAccountAuthorizedUserCredentialOptions, + universe_domain: universeDomain, + }); + + assert.equal(client.universeDomain, universeDomain); + }); + }); }); describe('getAccessToken()', () => { From fe982a2c00f6643510b3f744deb25713942deaf4 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 5 Sep 2023 19:12:36 +0200 Subject: [PATCH 452/662] chore(deps): update actions/checkout action to v4 (#1637) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6451c81a..31c4550b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [14, 16, 18, 20] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - uses: actions/setup-node@v3 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - uses: actions/setup-node@v3 with: node-version: 14 From 6543e0f3da95dcf5aa4e6fc4155757a1a6c32a46 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 7 Sep 2023 10:33:48 -0700 Subject: [PATCH 453/662] docs: Remove Outdated Electron Guidance (#1635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Remove outdated Electron guidance * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 85 ++++++++++++++++++------------------------- README.md | 13 ------- 2 files changed, 36 insertions(+), 62 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 21334d0a..9cae3d91 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -220,19 +220,6 @@ body: |- This method will throw if the token is invalid. - #### OAuth2 with Installed Apps (Electron) - If you're authenticating with OAuth2 from an installed application (like Electron), you may not want to embed your `client_secret` inside of the application sources. To work around this restriction, you can choose the `iOS` application type when creating your OAuth2 credentials in the [Google Developers console](https://console.cloud.google.com/): - - ![application type](https://user-images.githubusercontent.com/534619/36553844-3f9a863c-17b2-11e8-904a-29f6cd5f807a.png) - - If using the `iOS` type, when creating the OAuth2 client you won't need to pass a `client_secret` into the constructor: - ```js - const oAuth2Client = new OAuth2Client({ - clientId: , - redirectUri: - }); - ``` - ## JSON Web Tokens The Google Developers Console provides a `.json` file that you can use to configure a JWT auth client and authenticate your requests, for example when using a service account. @@ -501,12 +488,12 @@ body: |- The lifetime can be modified by changing the [session duration of the workforce pool](https://cloud.google.com/iam/docs/reference/rest/v1/locations.workforcePools), and can be set as high as 12 hours. #### Using Executable-sourced credentials with OIDC and SAML - + **Executable-sourced credentials** For executable-sourced credentials, a local executable is used to retrieve the 3rd party token. The executable must handle providing a valid, unexpired OIDC ID token or SAML assertion in JSON format to stdout. - + To use executable-sourced credentials, the `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES` environment variable must be set to `1`. @@ -535,12 +522,12 @@ body: |- - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. - `$SUBJECT_TOKEN_TYPE`: The subject token type. - `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program. - + The `--executable-timeout-millis` flag is optional. This is the duration for which the auth library will wait for the executable to finish, in milliseconds. Defaults to 30 seconds when not provided. The maximum allowed value is 2 minutes. The minimum is 5 seconds. - + The `--executable-output-file` flag is optional. If provided, the file path must point to the 3PI credential response generated by the executable. This is useful for caching the credentials. By specifying this path, the Auth libraries will first @@ -550,7 +537,7 @@ body: |- handle writing to this file - the auth libraries will only attempt to read from this location. The format of contents in the file should match the JSON format expected by the executable shown below. - + To retrieve the 3rd party token, the library will call the executable using the command specified. The executable's output must adhere to the response format specified below. It must output the response to stdout. @@ -623,17 +610,17 @@ body: |- The following security practices are highly recommended: * Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script. * The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion. - + Given the complexity of using executable-sourced credentials, it is recommended to use the existing supported mechanisms (file-sourced/URL-sourced) for providing 3rd party credentials unless they do not meet your specific requirements. - + You can now [use the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC or SAML provider. - + #### Configurable Token Lifetime When creating a credential configuration with workload identity federation using service account impersonation, you can provide an optional argument to configure the service account access token lifetime. - + To generate the configuration with configurable token lifetime, run the following command (this example uses an AWS configuration, but the token lifetime can be configured for all workload identity federation providers): ```bash @@ -645,14 +632,14 @@ body: |- --output-file /path/to/generated/config.json \ --service-account-token-lifetime-seconds $TOKEN_LIFETIME ``` - + Where the following variables need to be substituted: - `$PROJECT_NUMBER`: The Google Cloud project number. - `$POOL_ID`: The workload identity pool ID. - `$AWS_PROVIDER_ID`: The AWS provider ID. - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. - `$TOKEN_LIFETIME`: The desired lifetime duration of the service account access token in seconds. - + The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. The minimum allowed value is 600 (10 minutes) and the maximum allowed value is 43200 (12 hours). If a lifetime greater than one hour is required, the service account must be added as an allowed value in an Organization Policy that enforces the `constraints/iam.allowServiceAccountCredentialLifetimeExtension` constraint. @@ -660,35 +647,35 @@ body: |- Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired. ## Workforce Identity Federation - + [Workforce identity federation](https://cloud.google.com/iam/docs/workforce-identity-federation) lets you use an external identity provider (IdP) to authenticate and authorize a workforce—a group of users, such as employees, partners, and contractors—using IAM, so that the users can access Google Cloud services. Workforce identity federation extends Google Cloud's identity capabilities to support syncless, attribute-based single sign on. - + With workforce identity federation, your workforce can access Google Cloud resources using an external identity provider (IdP) that supports OpenID Connect (OIDC) or SAML 2.0 such as Azure Active Directory (Azure AD), Active Directory Federation Services (AD FS), Okta, and others. - + ### Accessing resources using an OIDC or SAML 2.0 identity provider - + In order to access Google Cloud resources from an identity provider that supports [OpenID Connect (OIDC)](https://openid.net/connect/), the following requirements are needed: - A workforce identity pool needs to be created. - An OIDC or SAML 2.0 identity provider needs to be added in the workforce pool. - + Follow the detailed [instructions](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation) on how to configure workforce identity federation. - + After configuring an OIDC or SAML 2.0 provider, a credential configuration file needs to be generated. The generated credential configuration file contains non-sensitive metadata to instruct the library on how to retrieve external subject tokens and exchange them for GCP access tokens. The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/). - + The Auth library can retrieve external subject tokens from a local file location (file-sourced credentials), from a local server (URL-sourced credentials) or by calling an executable (executable-sourced credentials). - + **File-sourced credentials** For file-sourced credentials, a background process needs to be continuously refreshing the file location with a new subject token prior to expiration. For tokens with one hour lifetimes, the token @@ -696,7 +683,7 @@ body: |- JSON format. To generate a file-sourced OIDC configuration, run the following command: - + ```bash # Generate an OIDC configuration file for file-sourced credentials. gcloud iam workforce-pools create-cred-config \ @@ -718,7 +705,7 @@ body: |- - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). To generate a file-sourced SAML configuration, run the following command: - + ```bash # Generate a SAML configuration file for file-sourced credentials. gcloud iam workforce-pools create-cred-config \ @@ -734,16 +721,16 @@ body: |- - `$PROVIDER_ID`: The provider ID. - `$PATH_TO_SAML_ASSERTION`: The file path used to retrieve the base64-encoded SAML assertion. - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). - + These commands generate the configuration file in the specified output file. - + **URL-sourced credentials** For URL-sourced credentials, a local server needs to host a GET endpoint to return the OIDC token. The response can be in plain text or JSON. Additional required request headers can also be specified. To generate a URL-sourced OIDC workforce identity configuration, run the following command: - + ```bash # Generate an OIDC configuration file for URL-sourced credentials. gcloud iam workforce-pools create-cred-config \ @@ -764,7 +751,7 @@ body: |- - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). To generate a URL-sourced SAML configuration, run the following command: - + ```bash # Generate a SAML configuration file for file-sourced credentials. gcloud iam workforce-pools create-cred-config \ @@ -775,7 +762,7 @@ body: |- --workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \ --output-file=/path/to/generated/config.json ``` - + These commands generate the configuration file in the specified output file. Where the following variables need to be substituted: @@ -787,17 +774,17 @@ body: |- - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). ### Using Executable-sourced workforce credentials with OIDC and SAML - + **Executable-sourced credentials** For executable-sourced credentials, a local executable is used to retrieve the 3rd party token. The executable must handle providing a valid, unexpired OIDC ID token or SAML assertion in JSON format to stdout. - + To use executable-sourced credentials, the `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES` environment variable must be set to `1`. To generate an executable-sourced workforce identity configuration, run the following command: - + ```bash # Generate a configuration file for executable-sourced credentials. gcloud iam workforce-pools create-cred-config \ @@ -820,12 +807,12 @@ body: |- - `$SUBJECT_TOKEN_TYPE`: The subject token type. - `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program. - `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). - + The `--executable-timeout-millis` flag is optional. This is the duration for which the auth library will wait for the executable to finish, in milliseconds. Defaults to 30 seconds when not provided. The maximum allowed value is 2 minutes. The minimum is 5 seconds. - + The `--executable-output-file` flag is optional. If provided, the file path must point to the 3rd party credential response generated by the executable. This is useful for caching the credentials. By specifying this path, the Auth libraries will first @@ -835,11 +822,11 @@ body: |- handle writing to this file - the auth libraries will only attempt to read from this location. The format of contents in the file should match the JSON format expected by the executable shown below. - + To retrieve the 3rd party token, the library will call the executable using the command specified. The executable's output must adhere to the response format specified below. It must output the response to stdout. - + Refer to the [using executable-sourced credentials with Workload Identity Federation](#using-executable-sourced-credentials-with-oidc-and-saml) above for the executable response specification. @@ -847,14 +834,14 @@ body: |- The following security practices are highly recommended: * Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script. * The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion. - + Given the complexity of using executable-sourced credentials, it is recommended to use the existing supported mechanisms (file-sourced/URL-sourced) for providing 3rd party credentials unless they do not meet your specific requirements. - + You can now [use the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC or SAML provider. - + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. diff --git a/README.md b/README.md index 83314534..999008f6 100644 --- a/README.md +++ b/README.md @@ -264,19 +264,6 @@ console.log(tokenInfo.scopes); This method will throw if the token is invalid. -#### OAuth2 with Installed Apps (Electron) -If you're authenticating with OAuth2 from an installed application (like Electron), you may not want to embed your `client_secret` inside of the application sources. To work around this restriction, you can choose the `iOS` application type when creating your OAuth2 credentials in the [Google Developers console](https://console.cloud.google.com/): - -![application type](https://user-images.githubusercontent.com/534619/36553844-3f9a863c-17b2-11e8-904a-29f6cd5f807a.png) - -If using the `iOS` type, when creating the OAuth2 client you won't need to pass a `client_secret` into the constructor: -```js -const oAuth2Client = new OAuth2Client({ - clientId: , - redirectUri: -}); -``` - ## JSON Web Tokens The Google Developers Console provides a `.json` file that you can use to configure a JWT auth client and authenticate your requests, for example when using a service account. From 49a5aec62ff5dddf38c55fd1f3c9502068e4e4e5 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:30:23 +0000 Subject: [PATCH 454/662] chore: correct path to node_modules (#1644) Source-Link: https://togithub.com/googleapis/synthtool/commit/8bc6db86e5064c36421923e310e25908b741b6eb Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:e7a37871068ab50b3841cb08619c288dc0665b62a7f39f2bdb7dea0ce6e89a3d --- .github/.OwlBot.lock.yaml | 4 ++-- .github/sync-repo-settings.yaml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index a3d003c6..5ddcbae8 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:e08f9a3757808cdaf7a377e962308c65c4d7eff12db206d4fae702dd50d43430 -# created: 2023-08-03T18:46:14.719706948Z + digest: sha256:e7a37871068ab50b3841cb08619c288dc0665b62a7f39f2bdb7dea0ce6e89a3d +# created: 2023-09-22T19:12:26.585611364Z diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 1350faef..b46e4c4d 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -7,7 +7,6 @@ branchProtectionRules: requiredStatusCheckContexts: - "ci/kokoro: Samples test" - "ci/kokoro: System test" - - docs - lint - test (14) - test (16) From 01da0d75835e54e880db05d1c1f0263a3fe3ac40 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 26 Sep 2023 20:04:27 +0200 Subject: [PATCH 455/662] chore(deps): update actions/checkout digest to 8ade135 (#1646) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 31c4550b..185d27d0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [14, 16, 18, 20] steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - uses: actions/setup-node@v3 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - uses: actions/setup-node@v3 with: node-version: 14 From 7030314c55c369acbda1873f649579b395ad07c8 Mon Sep 17 00:00:00 2001 From: its-meny Date: Thu, 28 Sep 2023 02:06:14 +0300 Subject: [PATCH 456/662] docs: Fix missing whitespace in README.md (#1649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Fix missing whitespace in README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 9cae3d91..d4c55051 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -13,7 +13,7 @@ body: |- - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials - This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. + This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. diff --git a/README.md b/README.md index 999008f6..ba9a6417 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This library provides a variety of ways to authenticate to your Google services. - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials -This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. +This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. From d76140f253c3903a22ed9087623be05e79a220f9 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 2 Oct 2023 15:40:31 -0700 Subject: [PATCH 457/662] feat: Expose `gcp-metadata` (#1655) --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index 0139d9fb..ea6c2764 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,8 @@ // limitations under the License. import {GoogleAuth} from './auth/googleauth'; +export * as gcpMetadata from 'gcp-metadata'; + export {AuthClient} from './auth/authclient'; export {Compute, ComputeOptions} from './auth/computeclient'; export { From 77eeaed1d66c89e855a15db0bb6b9dbd96179c8f Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 2 Oct 2023 15:45:58 -0700 Subject: [PATCH 458/662] fix: Workforce Audience Pattern (#1654) --- src/auth/baseexternalclient.ts | 2 +- test/test.baseexternalclient.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 8598cd69..a55f6489 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -57,7 +57,7 @@ export const CLOUD_RESOURCE_MANAGER = 'https://cloudresourcemanager.googleapis.com/v1/projects/'; /** The workforce audience pattern. */ const WORKFORCE_AUDIENCE_PATTERN = - '//iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; + '//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 826326a8..9d57c74b 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -184,6 +184,7 @@ describe('BaseExternalAccountClient', () => { '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', '//iam.googleapis.com/locations//workforcePools/pool/providers/provider', '//iam.googleapis.com/locations/workforcePools/pool/providers/provider', + '//iamAgoogleapisAcom/locations/global/workforcePools/workloadPools/providers/oidc', ]; const invalidExternalAccountOptionsWorkforceUserProject = Object.assign( {}, From 0bcacaf1ff95fde06dea7e3f7d1a4b8d044a6233 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 2 Oct 2023 15:50:15 -0700 Subject: [PATCH 459/662] docs(GoogleAuth): Document Credential Precedence (#1653) --- src/auth/googleauth.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index ca31fb13..994fefa3 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -173,6 +173,17 @@ export class GoogleAuth { */ static DefaultTransporter = DefaultTransporter; + /** + * Configuration is resolved in the following order of precedence: + * - {@link GoogleAuthOptions.credentials `credentials`} + * - {@link GoogleAuthOptions.keyFilename `keyFilename`} + * - {@link GoogleAuthOptions.keyFile `keyFile`} + * + * {@link GoogleAuthOptions.clientOptions `clientOptions`} are passed to the + * {@link AuthClient `AuthClient`s}. + * + * @param opts + */ constructor(opts?: GoogleAuthOptions) { opts = opts || {}; @@ -195,8 +206,13 @@ export class GoogleAuth { /** * Obtains the default project ID for the application. - * @param callback Optional callback - * @returns Promise that resolves with project Id (if used without callback) + * + * Retrieves in the following order of precedence: + * - The `projectId` provided in this object's construction + * - 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 */ getProjectId(): Promise; getProjectId(callback: ProjectIdCallback): void; @@ -900,8 +916,9 @@ export class GoogleAuth { } /** - * Automatically obtain a client based on the provided configuration. If no - * options were passed, use Application Default Credentials. + * Automatically obtain an {@link AuthClient `AuthClient`} based on the + * provided configuration. If no options were passed, use Application + * Default Credentials. */ async getClient() { if (!this.cachedCredential) { From 992397b5ef79dd5a4a0fe1383dc44ee71f11632e Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 2 Oct 2023 15:57:06 -0700 Subject: [PATCH 460/662] docs: Improve ID Token Documentation (#1651) --- samples/idTokenFromMetadataServer.js | 11 ++++++----- samples/idtokens-serverless.js | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/samples/idTokenFromMetadataServer.js b/samples/idTokenFromMetadataServer.js index 3454dcc3..e8127136 100644 --- a/samples/idTokenFromMetadataServer.js +++ b/samples/idTokenFromMetadataServer.js @@ -16,26 +16,27 @@ * Uses the Google Cloud metadata server environment to create an identity token * and add it to the HTTP request as part of an Authorization header. * - * @param {string} url - The url or target audience to obtain the ID token for. + * @param {string} targetAudience - The url or target audience to obtain the ID token for. */ -function main(url) { +function main(targetAudience) { // [START auth_cloud_idtoken_metadata_server] /** * TODO(developer): * 1. Uncomment and replace these variables before running the sample. */ - // const url = 'http://www.example.com'; + // const targetAudience = 'http://www.example.com'; const {GoogleAuth} = require('google-auth-library'); async function getIdTokenFromMetadataServer() { const googleAuth = new GoogleAuth(); - const client = await googleAuth.getClient(); + + const client = await googleAuth.getIdTokenClient(targetAudience); // Get the ID token. // Once you've obtained the ID token, you can use it to make an authenticated call // to the target audience. - await client.fetchIdToken(url); + await client.idTokenProvider.fetchIdToken(targetAudience); console.log('Generated ID token.'); } diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index 4c59f11f..5bbba470 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -62,6 +62,9 @@ function main( async function request() { console.info(`request ${url} with target audience ${targetAudience}`); const client = await auth.getIdTokenClient(targetAudience); + + // Alternatively, one can use `client.idTokenProvider.fetchIdToken` + // to return the ID Token. const res = await client.request({url}); console.info(res.data); } From ef29297215eb907c511627543391db9b43c1eac5 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 2 Oct 2023 16:07:04 -0700 Subject: [PATCH 461/662] feat: Expose Compute's Service Account Email (#1656) * feat: Expose Compute's Service Account Email * fix: Make `serviceAccountEmail` readonly --- src/auth/computeclient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index f4bc7a8f..29fabd16 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -33,7 +33,7 @@ export interface ComputeOptions extends RefreshOptions { } export class Compute extends OAuth2Client { - private serviceAccountEmail: string; + readonly serviceAccountEmail: string; scopes: string[]; /** From 4339d6410da7b792f96d31f62800507313c25430 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 2 Oct 2023 16:14:24 -0700 Subject: [PATCH 462/662] feat: Support Instantiating OAuth2Client with Credentials (#1652) --- src/auth/oauth2client.ts | 14 ++++++++------ test/test.oauth2.ts | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index a365277a..ec2212d7 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -404,12 +404,6 @@ export interface VerifyIdTokenOptions { maxExpiry?: number; } -export interface OAuth2ClientOptions extends RefreshOptions { - clientId?: string; - clientSecret?: string; - redirectUri?: string; -} - export interface RefreshOptions { // Eagerly refresh unexpired tokens when they are within this many // milliseconds from expiring". @@ -423,6 +417,13 @@ export interface RefreshOptions { forceRefreshOnFailure?: boolean; } +export interface OAuth2ClientOptions extends RefreshOptions { + clientId?: string; + clientSecret?: string; + redirectUri?: string; + credentials?: Credentials; +} + export class OAuth2Client extends AuthClient { private redirectUri?: string; private certificateCache: Certificates = {}; @@ -474,6 +475,7 @@ export class OAuth2Client extends AuthClient { this.eagerRefreshThresholdMillis = opts.eagerRefreshThresholdMillis || 5 * 60 * 1000; this.forceRefreshOnFailure = !!opts.forceRefreshOnFailure; + this.credentials = opts.credentials || {}; } protected static readonly GOOGLE_TOKEN_INFO_URL = diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 752c7ee4..146bbe65 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -23,7 +23,7 @@ import * as path from 'path'; import * as qs from 'querystring'; import * as sinon from 'sinon'; -import {CodeChallengeMethod, OAuth2Client} from '../src'; +import {CodeChallengeMethod, Credentials, OAuth2Client} from '../src'; import {LoginTicket} from '../src/auth/loginticket'; nock.disableNetConnect(); @@ -1593,5 +1593,32 @@ describe('oauth2', () => { /No access token is returned by the refreshHandler callback./ ); }); + + it('should accept and attempt to use an `access_token`', async () => { + const credentials: Credentials = {access_token: 'my-access-token'}; + const url = 'http://example.com'; + + const client = new OAuth2Client({credentials}); + + const scope = nock(url, { + reqheaders: { + Authorization: `Bearer ${credentials.access_token}`, + }, + }) + .get('/') + .reply(200); + + // We want the credentials object to be reference equal + assert.strict.equal(client.credentials, credentials); + + await client.request({url}); + + scope.done(); + + // The access token should still be available after use + assert.strict.deepEqual(await client.getAccessToken(), { + token: credentials.access_token, + }); + }); }); }); From 5f296c37390ba71aa3aa1e4259ff7c525eeaad7c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:19:18 -0700 Subject: [PATCH 463/662] chore(main): release 9.1.0 (#1619) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f887cee..032a26e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.1.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.0.0...v9.1.0) (2023-10-02) + + +### Features + +* Byoid metrics ([#1595](https://github.com/googleapis/google-auth-library-nodejs/issues/1595)) ([fe09d6b](https://github.com/googleapis/google-auth-library-nodejs/commit/fe09d6b4eb5a8215d21564a97bcc7ea5beb570bf)) +* Expose `gcp-metadata` ([#1655](https://github.com/googleapis/google-auth-library-nodejs/issues/1655)) ([d76140f](https://github.com/googleapis/google-auth-library-nodejs/commit/d76140f253c3903a22ed9087623be05e79a220f9)) +* Expose Compute's Service Account Email ([#1656](https://github.com/googleapis/google-auth-library-nodejs/issues/1656)) ([ef29297](https://github.com/googleapis/google-auth-library-nodejs/commit/ef29297215eb907c511627543391db9b43c1eac5)) +* Extend `universe_domain` support ([#1633](https://github.com/googleapis/google-auth-library-nodejs/issues/1633)) ([4ee02da](https://github.com/googleapis/google-auth-library-nodejs/commit/4ee02da4f6934eeb9550b8b659d1abda85194e6b)) +* Support Instantiating OAuth2Client with Credentials ([#1652](https://github.com/googleapis/google-auth-library-nodejs/issues/1652)) ([4339d64](https://github.com/googleapis/google-auth-library-nodejs/commit/4339d6410da7b792f96d31f62800507313c25430)) + + +### Bug Fixes + +* **deps:** Update dependency @google-cloud/storage to v7 ([#1629](https://github.com/googleapis/google-auth-library-nodejs/issues/1629)) ([8075b6f](https://github.com/googleapis/google-auth-library-nodejs/commit/8075b6fbb347067ec231134fb4388ab9bcd1bde0)) +* **deps:** Update dependency @googleapis/iam to v11 ([#1622](https://github.com/googleapis/google-auth-library-nodejs/issues/1622)) ([03c0ac9](https://github.com/googleapis/google-auth-library-nodejs/commit/03c0ac9950217ec08ac9ad66eaaa4e9a9e67d423)) +* **deps:** Update dependency google-auth-library to v9 ([#1618](https://github.com/googleapis/google-auth-library-nodejs/issues/1618)) ([1873fef](https://github.com/googleapis/google-auth-library-nodejs/commit/1873fefcdf4df00ff40b0f311e40dcdd402f799e)) +* **deps:** Update dependency puppeteer to v21 ([#1627](https://github.com/googleapis/google-auth-library-nodejs/issues/1627)) ([dd04fbd](https://github.com/googleapis/google-auth-library-nodejs/commit/dd04fbdeb17e457a09b06a4abc2aedf511f355ea)) +* Workforce Audience Pattern ([#1654](https://github.com/googleapis/google-auth-library-nodejs/issues/1654)) ([77eeaed](https://github.com/googleapis/google-auth-library-nodejs/commit/77eeaed1d66c89e855a15db0bb6b9dbd96179c8f)) + ## [9.0.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v8.9.0...v9.0.0) (2023-07-20) diff --git a/package.json b/package.json index 113c798a..5678d5e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.0.0", + "version": "9.1.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 8bd760eb..19b63c6a 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^11.0.0", - "google-auth-library": "^9.0.0", + "google-auth-library": "^9.1.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 83fdd7c574cf6ce0468b65240957e9d8aa8b3e2b Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 6 Oct 2023 14:46:27 -0700 Subject: [PATCH 464/662] test: Flaky Test Fixes (#1647) * test: remove duplicated `gcp-metadata` tests * chore: remove unused * chore: lint fixes --- .../externalAccountAuthorizedUserClient.ts | 5 +- src/auth/googleauth.ts | 5 +- src/auth/identitypoolclient.ts | 5 +- src/auth/oauth2client.ts | 5 +- src/auth/pluggable-auth-client.ts | 5 +- src/auth/stscredentials.ts | 5 +- test/test.googleauth.ts | 54 +------------------ test/test.oauth2.ts | 15 +++--- 8 files changed, 19 insertions(+), 80 deletions(-) diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index 3c05cc56..e6eca331 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -124,9 +124,8 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { this.applyClientAuthenticationOptions(opts); try { - const response = await this.transporter.request( - opts - ); + const response = + await this.transporter.request(opts); // Successful response. const tokenRefreshResponse = response.data; tokenRefreshResponse.res = response; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 994fefa3..3baea078 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -355,9 +355,8 @@ export class GoogleAuth { } // Look in the well-known credential file location. - credential = await this._tryGetApplicationCredentialsFromWellKnownFile( - options - ); + credential = + await this._tryGetApplicationCredentialsFromWellKnownFile(options); if (credential) { if (credential instanceof JWT) { credential.scopes = this.scopes; diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index 38968bd5..06c1e9b5 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -218,9 +218,8 @@ export class IdentityPoolClient extends BaseExternalAccountClient { const response = await this.transporter.request(opts); subjectToken = response.data; } else if (formatType === 'json' && formatSubjectTokenFieldName) { - const response = await this.transporter.request( - opts - ); + const response = + await this.transporter.request(opts); subjectToken = response.data[formatSubjectTokenFieldName]; } if (!subjectToken) { diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index ec2212d7..fc0fddc7 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -590,9 +590,8 @@ export class OAuth2Client extends AuthClient { .replace(/=/g, '_') .replace(/\//g, '-'); // Generate the base64 encoded SHA256 - const unencodedCodeChallenge = await crypto.sha256DigestBase64( - codeVerifier - ); + const unencodedCodeChallenge = + await crypto.sha256DigestBase64(codeVerifier); // We need to use base64UrlEncoding instead of standard base64 const codeChallenge = unencodedCodeChallenge .split('=')[0] diff --git a/src/auth/pluggable-auth-client.ts b/src/auth/pluggable-auth-client.ts index 8fb5a8f7..ad31c78a 100644 --- a/src/auth/pluggable-auth-client.ts +++ b/src/auth/pluggable-auth-client.ts @@ -281,9 +281,8 @@ export class PluggableAuthClient extends BaseExternalAccountClient { serviceAccountEmail ); } - executableResponse = await this.handler.retrieveResponseFromExecutable( - envMap - ); + executableResponse = + await this.handler.retrieveResponseFromExecutable(envMap); } if (executableResponse.version > MAXIMUM_EXECUTABLE_VERSION) { diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 19ec95bd..44f431b7 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -207,9 +207,8 @@ export class StsCredentials extends OAuthClientAuthHandler { this.applyClientAuthenticationOptions(opts); try { - const response = await this.transporter.request( - opts - ); + const response = + await this.transporter.request(opts); // Successful response. const stsSuccessfulResponse = response.data; stsSuccessfulResponse.res = response; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index c094ed42..e22556d8 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -54,10 +54,7 @@ import { } from './externalclienthelper'; import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; import {AuthClient} from '../src/auth/authclient'; -import { - ExternalAccountAuthorizedUserClient, - ExternalAccountAuthorizedUserClientOptions, -} from '../src/auth/externalAccountAuthorizedUserClient'; +import {ExternalAccountAuthorizedUserClient} from '../src/auth/externalAccountAuthorizedUserClient'; nock.disableNetConnect(); @@ -70,7 +67,6 @@ describe('googleauth', () => { const svcAccountPath = `${instancePath}/service-accounts/?recursive=true`; const API_KEY = 'test-123'; const PEM_PATH = './test/fixtures/private.pem'; - const P12_PATH = './test/fixtures/key.p12'; const STUB_PROJECT = 'my-awesome-project'; const ENDPOINT = '/events:report'; const RESPONSE_BODY = 'RESPONSE_BODY'; @@ -240,24 +236,6 @@ describe('googleauth', () => { }; } - function nock500GCE() { - const primary = nock(host).get(instancePath).reply(500, {}, HEADERS); - const secondary = nock(SECONDARY_HOST_ADDRESS) - .get(instancePath) - .reply(500, {}, HEADERS); - - return { - done: () => { - try { - primary.done(); - secondary.done(); - } catch (err) { - // secondary can sometimes complete prior to primary. - } - }, - }; - } - function createGetProjectIdNock(projectId = 'not-real') { return nock(host) .get(`${BASE_PATH}/project/project-id`) @@ -1173,36 +1151,6 @@ describe('googleauth', () => { scopes.forEach(s => s.done()); }); - it('getCredentials should error if metadata server is not reachable', async () => { - const scopes = [ - nockIsGCE(), - createGetProjectIdNock(), - nock(HOST_ADDRESS).get(svcAccountPath).reply(404), - ]; - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - await assert.rejects( - auth.getCredentials(), - /Unsuccessful response status code. Request failed with status code 404/ - ); - scopes.forEach(s => s.done()); - }); - - it('getCredentials should error if body is empty', async () => { - const scopes = [ - nockIsGCE(), - createGetProjectIdNock(), - nock(HOST_ADDRESS).get(svcAccountPath).reply(200, {}), - ]; - await auth._checkIsGCE(); - assert.strictEqual(true, auth.isGCE); - await assert.rejects( - auth.getCredentials(), - /Invalid response from metadata service: incorrect Metadata-Flavor header./ - ); - scopes.forEach(s => s.done()); - }); - it('getCredentials should handle valid environment variable', async () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 146bbe65..6156b8aa 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1478,9 +1478,8 @@ describe('oauth2', () => { }; assert.deepStrictEqual(client.credentials, {}); - const requestMetaData = await client.getRequestHeaders( - 'http://example.com' - ); + const requestMetaData = + await client.getRequestHeaders('http://example.com'); assert.deepStrictEqual(requestMetaData, expectedMetadata); }); @@ -1501,9 +1500,8 @@ describe('oauth2', () => { Authorization: 'Bearer access_token', }; - const requestMetaData = await client.getRequestHeaders( - 'http://example.com' - ); + const requestMetaData = + await client.getRequestHeaders('http://example.com'); assert.deepStrictEqual(requestMetaData, expectedMetadata); }); @@ -1517,9 +1515,8 @@ describe('oauth2', () => { Authorization: 'Bearer initial-access-token', }; - const requestMetaData = await client.getRequestHeaders( - 'http://example.com' - ); + const requestMetaData = + await client.getRequestHeaders('http://example.com'); assert.deepStrictEqual(requestMetaData, expectedMetadata); }); From 37291d5698615a38c3bad25cc5fb77eb5cf11cd8 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:51:56 -0700 Subject: [PATCH 465/662] chore(nodejs): Add `system-test/fixtures` to `.eslintignore` (#1662) * fix: Add `system-test/fixtures` to `.eslintignore` * refactor: Use `**` Source-Link: https://github.com/googleapis/synthtool/commit/b7858ba70e8acabc89d13558a71dd9318a57034a Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:abc68a9bbf4fa808b25fa16d3b11141059dc757dbc34f024744bba36c200b40f Co-authored-by: Owl Bot Co-authored-by: Daniel Bankhead --- .eslintignore | 1 + .github/.OwlBot.lock.yaml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintignore b/.eslintignore index ea5b04ae..c4a0963e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ build/ docs/ protos/ samples/generated/ +system-test/**/fixtures diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 5ddcbae8..40b49d2b 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:e7a37871068ab50b3841cb08619c288dc0665b62a7f39f2bdb7dea0ce6e89a3d -# created: 2023-09-22T19:12:26.585611364Z + digest: sha256:abc68a9bbf4fa808b25fa16d3b11141059dc757dbc34f024744bba36c200b40f +# created: 2023-10-04T20:56:40.710775365Z From 311d6a1e31a0e4a875f8d1138ba45d654db6af5f Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 6 Oct 2023 16:20:00 -0700 Subject: [PATCH 466/662] refactor: Replace LRU Dependency With Custom Implementation (#1659) * build: remove `lru-cache` * feat: Add Custom LRU * fix: remove log * refactor: Use `Map` instead of Linked List * chore: Move `LRU` to a `util.ts` * chore: Rename for clarity * test: Add `LRUCache` tests * chore: use utility LRUCache * fix: use manual Promise `setTimeout` 'timers/promises' is not available in Node 14 * chore: lint * docs: LRUCache --- package.json | 4 +- src/auth/jwtaccess.ts | 6 +-- src/util.ts | 110 ++++++++++++++++++++++++++++++++++++++++++ test/test.util.ts | 67 +++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 src/util.ts create mode 100644 test/test.util.ts diff --git a/package.json b/package.json index 5678d5e9..24c1d4ab 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,13 @@ "gaxios": "^6.0.0", "gcp-metadata": "^6.0.0", "gtoken": "^7.0.0", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" + "jws": "^4.0.0" }, "devDependencies": { "@compodoc/compodoc": "^1.1.7", "@types/base64-js": "^1.2.5", "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", - "@types/lru-cache": "^5.0.0", "@types/mocha": "^9.0.0", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index 88f792dd..b7c61147 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -13,11 +13,11 @@ // limitations under the License. import * as jws from 'jws'; -import * as LRU from 'lru-cache'; import * as stream from 'stream'; import {JWTInput} from './credentials'; import {Headers} from './oauth2client'; +import {LRUCache} from '../util'; const DEFAULT_HEADER: jws.Header = { alg: 'RS256', @@ -35,8 +35,8 @@ export class JWTAccess { projectId?: string; eagerRefreshThresholdMillis: number; - private cache = new LRU({ - max: 500, + private cache = new LRUCache<{expiration: number; headers: Headers}>({ + capacity: 500, maxAge: 60 * 60 * 1000, }); diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 00000000..a9cd68b3 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,110 @@ +// Copyright 2023 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. + +export interface LRUCacheOptions { + /** + * The maximum number of items to cache. + */ + capacity: number; + /** + * An optional max age for items in milliseconds. + */ + maxAge?: number; +} + +/** + * A simple LRU cache utility. + * Not meant for external usage. + * + * @experimental + * @internal + */ +export class LRUCache { + readonly capacity: number; + + /** + * Maps are in order. Thus, the older item is the first item. + * + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map} + */ + #cache = new Map(); + maxAge?: number; + + constructor(options: LRUCacheOptions) { + this.capacity = options.capacity; + this.maxAge = options.maxAge; + } + + /** + * Moves the key to the end of the cache. + * + * @param key the key to move + * @param value the value of the key + */ + #moveToEnd(key: string, value: T) { + this.#cache.delete(key); + this.#cache.set(key, { + value, + lastAccessed: Date.now(), + }); + } + + /** + * Add an item to the cache. + * + * @param key the key to upsert + * @param value the value of the key + */ + set(key: string, value: T) { + this.#moveToEnd(key, value); + this.#evict(); + } + + /** + * Get an item from the cache. + * + * @param key the key to retrieve + */ + get(key: string): T | undefined { + const item = this.#cache.get(key); + if (!item) return; + + this.#moveToEnd(key, item.value); + this.#evict(); + + return item.value; + } + + /** + * Maintain the cache based on capacity and TTL. + */ + #evict() { + const cutoffDate = this.maxAge ? Date.now() - this.maxAge : 0; + + /** + * Because we know Maps are in order, this item is both the + * last item in the list (capacity) and oldest (maxAge). + */ + let oldestItem = this.#cache.entries().next(); + + while ( + !oldestItem.done && + (this.#cache.size > this.capacity || // too many + oldestItem.value[1].lastAccessed < cutoffDate) // too old + ) { + this.#cache.delete(oldestItem.value[0]); + oldestItem = this.#cache.entries().next(); + } + } +} diff --git a/test/test.util.ts b/test/test.util.ts new file mode 100644 index 00000000..6871c621 --- /dev/null +++ b/test/test.util.ts @@ -0,0 +1,67 @@ +// Copyright 2023 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. + +import {strict as assert} from 'assert'; + +import {LRUCache} from '../src/util'; + +describe('util', () => { + describe('LRUCache', () => { + it('should set and get a cached item', () => { + const expected = 'value'; + const lru = new LRUCache({capacity: 5}); + lru.set('sample', expected); + + assert.equal(lru.get('sample'), expected); + }); + + it('should evict oldest items when over capacity', () => { + const capacity = 5; + const overCapacity = 2; + + const lru = new LRUCache({capacity}); + + for (let i = 0; i < capacity + overCapacity; i++) { + lru.set(`${i}`, i); + } + + // the first few shouldn't be there + for (let i = 0; i < overCapacity; i++) { + assert.equal(lru.get(`${i}`), undefined); + } + + // the rest should be there + for (let i = overCapacity; i < capacity + overCapacity; i++) { + assert.equal(lru.get(`${i}`), i); + } + }); + + it('should evict items older than a supplied `maxAge`', async () => { + const maxAge = 50; + + const lru = new LRUCache({capacity: 5, maxAge}); + + lru.set('first', 1); + lru.set('second', 2); + + await new Promise(res => setTimeout(res, maxAge + 1)); + + lru.set('third', 3); + + assert.equal(lru.get('first'), undefined); + assert.equal(lru.get('second'), undefined); + assert.equal(lru.get('third'), 3); + }); + }); +}); From b25d4f54f88c52c8496ef65f5ab2a75122100f2c Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 11 Oct 2023 19:30:43 -0700 Subject: [PATCH 467/662] fix: DOS security risks (#1668) --- src/auth/baseexternalclient.ts | 10 ++++++++++ src/auth/googleauth.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index a55f6489..8924dedb 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -238,6 +238,16 @@ export abstract class BaseExternalAccountClient extends AuthClient { /** The service account email to be impersonated, if available. */ getServiceAccountEmail(): string | null { if (this.serviceAccountImpersonationUrl) { + if (this.serviceAccountImpersonationUrl.length > 256) { + /** + * Prevents DOS attacks. + * @see {@link https://github.com/googleapis/google-auth-library-nodejs/security/code-scanning/84} + **/ + throw new RangeError( + `URL is too long: ${this.serviceAccountImpersonationUrl}` + ); + } + // Parse email from URL. The formal looks as follows: // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken const re = /serviceAccounts\/(?[^:]+):generateAccessToken$/; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 3baea078..7167c416 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -569,6 +569,16 @@ export class GoogleAuth { json.source_credentials.refresh_token ); + if (json.service_account_impersonation_url?.length > 256) { + /** + * Prevents DOS attacks. + * @see {@link https://github.com/googleapis/google-auth-library-nodejs/security/code-scanning/85} + **/ + throw new RangeError( + `Target principal is too long: ${json.service_account_impersonation_url}` + ); + } + // Extreact service account from service_account_impersonation_url const targetPrincipal = /(?[^/]+):generateAccessToken$/.exec( json.service_account_impersonation_url From 4f94ffe474ee41b560813b81e8d621be848d38d0 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 12 Oct 2023 16:56:51 +0200 Subject: [PATCH 468/662] fix(deps): update dependency @googleapis/iam to v12 (#1671) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 19b63c6a..c732af44 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^11.0.0", + "@googleapis/iam": "^12.0.0", "google-auth-library": "^9.1.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 56cb3ad24c5e4b6952169b88d0a43e89cbf1252e Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 12 Oct 2023 15:52:18 -0700 Subject: [PATCH 469/662] fix: Remove broken source maps (#1669) * fix: Remove broken source maps * feat: keep source maps for local debugging * fix: add missing arg separator `--` --- package.json | 4 ++-- tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 24c1d4ab..0ca5eafd 100644 --- a/package.json +++ b/package.json @@ -73,12 +73,12 @@ "lint": "gts check", "compile": "tsc -p .", "fix": "gts fix", - "pretest": "npm run compile", + "pretest": "npm run compile -- --sourceMap", "docs": "compodoc src/", "samples-setup": "cd samples/ && npm link ../ && npm run setup && cd ../", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "system-test": "mocha build/system-test --timeout 60000", - "presystem-test": "npm run compile", + "presystem-test": "npm run compile -- --sourceMap", "webpack": "webpack", "browser-test": "karma start", "docs-test": "linkinator docs", diff --git a/tsconfig.json b/tsconfig.json index e4058efd..6416b355 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "lib": ["es2018", "dom"], "rootDir": ".", - "outDir": "build" + "outDir": "build", + "sourceMap": false }, "include": [ "src/*.ts", From 6c0a6bdc156c1098b09e85e82cabadaa73e7367f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 21 Oct 2023 00:09:19 +0200 Subject: [PATCH 470/662] chore(deps): update actions/checkout digest to b4ffde6 (#1678) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 185d27d0..01a63060 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [14, 16, 18, 20] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - uses: actions/setup-node@v3 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - uses: actions/setup-node@v3 with: node-version: 14 From edb94018cadd2ad796ccec5496e3bfcbade39c7f Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 24 Oct 2023 07:17:06 -0700 Subject: [PATCH 471/662] fix: increase max asset size (#1682) * fix: increase max asset size * chore: copywrite header --- .../fixtures/kitchen/webpack.config.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/system-test/fixtures/kitchen/webpack.config.js b/system-test/fixtures/kitchen/webpack.config.js index b624d9d0..82aee87a 100644 --- a/system-test/fixtures/kitchen/webpack.config.js +++ b/system-test/fixtures/kitchen/webpack.config.js @@ -1,6 +1,27 @@ +// 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. + +// Use `npm run webpack` to produce Webpack bundle for this library. + const path = require('path'); module.exports = { + performance: { + hints: false, + maxEntrypointSize: 524288, + maxAssetSize: 524288, + }, entry: './src/index.ts', resolve: { extensions: ['.ts', '.js', '.json'], From 5ac67052f6c19e93c3e8c4e1636fad4737fcee08 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 26 Oct 2023 15:58:43 -0700 Subject: [PATCH 472/662] feat: Unify Base `AuthClient` Options (#1663) * feat: Expose `Gaxios` instance and default * feat: Unify Base `AuthClient` Options * docs: clean up documentation * chore: Discourage external use via `@internal` * refactor: minor * refactor: clean up `transporter` interface, options, and docs * Merge branch 'main' of github.com:googleapis/google-auth-library-nodejs into authclient-enhancements * Merge branch 'main' of github.com:googleapis/google-auth-library-nodejs into authclient-enhancements * test: Init tests for `AuthClient` * fix: Use entire `JWT` to create accurate `createScoped` * chore: update `typescript` in fixtures * test: Use Sinon Fake Timer to Avoid Flaky Time Issue (#1667) * test: Use Sinon Fake Timer to Avoid Flaky Time Issue * chore: change order for clarity * docs(sample): Improve `keepAlive` sample with `transporterOptions` * docs: Docs-deprecate `additionalOptions` * refactor: un-alias `getOriginalOrCamel` * chore: remove confusing duplicate interface * docs: nit * docs: Improve camelCased option documentation We can refactor once https://github.com/microsoft/TypeScript/issues/50715 has been resolved. * refactor: Unify `OriginalAndCamel` & `SnakeToCamelObject` + make recursive * docs: Add issue tracking link * refactor: Replace `getOriginalOrCamel` with a cleaner API * feat: Allow optional `obj` * refactor: re-add `SnakeToCamelObject` * refactor: dedupe interface for now * feat: Add camelCase options for `IdentityPoolClient` --- samples/keepalive.js | 17 ++- src/auth/authclient.ts | 137 +++++++++++++++-- src/auth/awsclient.ts | 15 +- src/auth/baseexternalclient.ts | 109 +++++++------- src/auth/computeclient.ts | 8 +- src/auth/downscopedclient.ts | 30 +--- .../externalAccountAuthorizedUserClient.ts | 17 +-- src/auth/externalclient.ts | 11 +- src/auth/googleauth.ts | 44 +++--- src/auth/identitypoolclient.ts | 43 ++++-- src/auth/idtokenclient.ts | 11 +- src/auth/impersonated.ts | 10 +- src/auth/jwtclient.ts | 24 ++- src/auth/oauth2client.ts | 38 ++--- src/auth/pluggable-auth-client.ts | 11 +- src/auth/refreshclient.ts | 15 +- src/transporters.ts | 18 ++- src/util.ts | 140 ++++++++++++++++++ system-test/fixtures/kitchen/package.json | 2 +- test/test.authclient.ts | 60 ++++++++ test/test.baseexternalclient.ts | 4 +- test/test.util.ts | 20 ++- 22 files changed, 566 insertions(+), 218 deletions(-) create mode 100644 test/test.authclient.ts diff --git a/samples/keepalive.js b/samples/keepalive.js index 2cab0e19..abfabc34 100644 --- a/samples/keepalive.js +++ b/samples/keepalive.js @@ -30,21 +30,26 @@ const https = require('https'); * Acquire a client, and make a request to an API that's enabled by default. */ async function main() { + // create a new agent with keepAlive enabled. + const agent = new https.Agent({keepAlive: true}); + const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform', + clientOptions: { + transporterOptions: { + agent, + }, + }, }); const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - // create a new agent with keepAlive enabled - const agent = new https.Agent({keepAlive: true}); - - // use the agent as an Axios config param to make the request - const res = await client.request({url, agent}); + // the agent uses the provided agent. + const res = await client.request({url}); console.log(res.data); - // Re-use the same agent to make the next request over the same connection + // Can also use another agent per-request. const res2 = await client.request({url, agent}); console.log(res2.data); } diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index f41a1537..12033203 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -13,31 +13,116 @@ // limitations under the License. import {EventEmitter} from 'events'; -import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; +import {Gaxios, GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; import {DefaultTransporter, Transporter} from '../transporters'; import {Credentials} from './credentials'; import {Headers} from './oauth2client'; +import {OriginalAndCamel, originalOrCamelOptions} from '../util'; /** - * Defines the root interface for all clients that generate credentials - * for calling Google APIs. All clients should implement this interface. + * Base auth configurations (e.g. from JWT or `.json` files) with conventional + * camelCased options. + * + * @privateRemarks + * + * This interface is purposely not exported so that it can be removed once + * {@link https://github.com/microsoft/TypeScript/issues/50715} has been + * resolved. Then, we can use {@link OriginalAndCamel} to shrink this interface. + * + * Tracking: {@link https://github.com/googleapis/google-auth-library-nodejs/issues/1686} */ -export interface CredentialsClient { +interface AuthJSONOptions { /** * The project ID corresponding to the current credentials if available. */ - projectId?: string | null; + project_id: string | null; + /** + * An alias for {@link AuthJSONOptions.project_id `project_id`}. + */ + projectId: AuthJSONOptions['project_id']; + + /** + * The quota project ID. The quota project can be used by client libraries for the billing purpose. + * See {@link https://cloud.google.com/docs/quota Working with quotas} + */ + quota_project_id: string; + + /** + * An alias for {@link AuthJSONOptions.quota_project_id `quota_project_id`}. + */ + quotaProjectId: AuthJSONOptions['quota_project_id']; + + /** + * The default service domain for a given Cloud universe. + */ + universe_domain: string; + + /** + * An alias for {@link AuthJSONOptions.universe_domain `universe_domain`}. + */ + universeDomain: AuthJSONOptions['universe_domain']; +} + +/** + * Base `AuthClient` configuration. + * + * The camelCased options are aliases of the snake_cased options, supporting both + * JSON API and JS conventions. + */ +export interface AuthClientOptions + extends Partial> { + credentials?: Credentials; + + /** + * A `Gaxios` or `Transporter` instance to use for `AuthClient` requests. + */ + transporter?: Gaxios | Transporter; + + /** + * Provides default options to the transporter, such as {@link GaxiosOptions.agent `agent`} or + * {@link GaxiosOptions.retryConfig `retryConfig`}. + */ + transporterOptions?: GaxiosOptions; /** - * The expiration threshold in milliseconds before forcing token refresh. + * The expiration threshold in milliseconds before forcing token refresh of + * unexpired tokens. */ - eagerRefreshThresholdMillis: number; + eagerRefreshThresholdMillis?: number; /** - * Whether to force refresh on failure when making an authorization request. + * Whether to attempt to refresh tokens on status 401/403 responses + * even if an attempt is made to refresh the token preemptively based + * on the expiry_date. */ - forceRefreshOnFailure: boolean; + forceRefreshOnFailure?: boolean; +} + +/** + * The default cloud universe + * + * @see {@link AuthJSONOptions.universe_domain} + */ +export const DEFAULT_UNIVERSE = 'googleapis.com'; + +/** + * The default {@link AuthClientOptions.eagerRefreshThresholdMillis} + */ +export const DEFAULT_EAGER_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000; + +/** + * Defines the root interface for all clients that generate credentials + * for calling Google APIs. All clients should implement this interface. + */ +export interface CredentialsClient { + projectId?: AuthClientOptions['projectId']; + eagerRefreshThresholdMillis: NonNullable< + AuthClientOptions['eagerRefreshThresholdMillis'] + >; + forceRefreshOnFailure: NonNullable< + AuthClientOptions['forceRefreshOnFailure'] + >; /** * @return A promise that resolves with the current GCP access token @@ -88,16 +173,42 @@ export abstract class AuthClient extends EventEmitter implements CredentialsClient { + projectId?: string | null; /** * The quota project ID. The quota project can be used by client libraries for the billing purpose. - * See {@link https://cloud.google.com/docs/quota| Working with quotas} + * See {@link https://cloud.google.com/docs/quota Working with quotas} */ quotaProjectId?: string; - transporter: Transporter = new DefaultTransporter(); + transporter: Transporter; credentials: Credentials = {}; - projectId?: string | null; - eagerRefreshThresholdMillis = 5 * 60 * 1000; + eagerRefreshThresholdMillis = DEFAULT_EAGER_REFRESH_THRESHOLD_MILLIS; forceRefreshOnFailure = false; + universeDomain = DEFAULT_UNIVERSE; + + constructor(opts: AuthClientOptions = {}) { + super(); + + const options = originalOrCamelOptions(opts); + + // Shared auth options + this.projectId = options.get('project_id') ?? null; + this.quotaProjectId = options.get('quota_project_id'); + this.credentials = options.get('credentials') ?? {}; + this.universeDomain = options.get('universe_domain') ?? DEFAULT_UNIVERSE; + + // Shared client options + this.transporter = opts.transporter ?? new DefaultTransporter(); + + if (opts.transporterOptions) { + this.transporter.defaults = opts.transporterOptions; + } + + if (opts.eagerRefreshThresholdMillis) { + this.eagerRefreshThresholdMillis = opts.eagerRefreshThresholdMillis; + } + + this.forceRefreshOnFailure = opts.forceRefreshOnFailure ?? false; + } /** * Provides an alternative Gaxios request implementation with auth credentials diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index 4ab39182..bd47c7c4 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -19,7 +19,8 @@ import { BaseExternalAccountClient, BaseExternalAccountClientOptions, } from './baseexternalclient'; -import {RefreshOptions, Headers} from './oauth2client'; +import {Headers} from './oauth2client'; +import {AuthClientOptions} from './authclient'; /** * AWS credentials JSON interface. This is used for AWS workloads. @@ -81,11 +82,15 @@ export class AwsClient extends BaseExternalAccountClient { * An error is thrown if the credential is not a valid AWS credential. * @param options The external account options object typically loaded * from the external account JSON credential file. - * @param additionalOptions Optional additional behavior customization - * options. These currently customize expiration threshold time and - * whether to retry on 401/403 API request errors. + * @param additionalOptions **DEPRECATED, all options are available in the + * `options` parameter.** Optional additional behavior customization options. + * These currently customize expiration threshold time and whether to retry + * on 401/403 API request errors. */ - constructor(options: AwsClientOptions, additionalOptions?: RefreshOptions) { + constructor( + options: AwsClientOptions, + additionalOptions?: AuthClientOptions + ) { super(options, additionalOptions); this.environmentId = options.credential_source.environment_id; // This is only required if the AWS region is not available in the diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 8924dedb..6496b143 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -21,11 +21,12 @@ import { import * as stream from 'stream'; import {Credentials} from './credentials'; -import {AuthClient} from './authclient'; +import {AuthClient, AuthClientOptions} from './authclient'; import {BodyResponseCallback} from '../transporters'; -import {GetAccessTokenResponse, Headers, RefreshOptions} from './oauth2client'; +import {GetAccessTokenResponse, Headers} from './oauth2client'; import * as sts from './stscredentials'; import {ClientAuthentication} from './oauth2common'; +import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; /** * The required token exchange grant_type: rfc8693#section-2.1 @@ -63,18 +64,13 @@ const WORKFORCE_AUDIENCE_PATTERN = const pkg = require('../../../package.json'); /** - * The default cloud universe + * For backwards compatibility. */ -export const DEFAULT_UNIVERSE = 'googleapis.com'; +export {DEFAULT_UNIVERSE} from './authclient'; -export interface SharedExternalAccountClientOptions { +export interface SharedExternalAccountClientOptions extends AuthClientOptions { audience: string; token_url: string; - quota_project_id?: string; - /** - * universe domain is the default service domain for a given Cloud universe - */ - universe_domain?: string; } /** @@ -151,51 +147,69 @@ export abstract class BaseExternalAccountClient extends AuthClient { private readonly stsCredential: sts.StsCredentials; private readonly clientAuth?: ClientAuthentication; private readonly workforcePoolUserProject?: string; - public universeDomain = DEFAULT_UNIVERSE; - public projectId: string | null; public projectNumber: string | null; - public readonly eagerRefreshThresholdMillis: number; - public readonly forceRefreshOnFailure: boolean; private readonly configLifetimeRequested: boolean; protected credentialSourceType?: string; /** * Instantiate a BaseExternalAccountClient instance using the provided JSON * object loaded from an external account credentials file. * @param options The external account options object typically loaded - * from the external account JSON credential file. - * @param additionalOptions Optional additional behavior customization - * options. These currently customize expiration threshold time and - * whether to retry on 401/403 API request errors. + * from the external account JSON credential file. The camelCased options + * are aliases for the snake_cased options. + * @param additionalOptions **DEPRECATED, all options are available in the + * `options` parameter.** Optional additional behavior customization options. + * These currently customize expiration threshold time and whether to retry + * on 401/403 API request errors. */ constructor( - options: BaseExternalAccountClientOptions, - additionalOptions?: RefreshOptions + options: + | BaseExternalAccountClientOptions + | SnakeToCamelObject, + additionalOptions?: AuthClientOptions ) { - super(); - if (options.type !== EXTERNAL_ACCOUNT_TYPE) { + super({...options, ...additionalOptions}); + + const opts = originalOrCamelOptions( + options as BaseExternalAccountClientOptions + ); + + if (opts.get('type') !== EXTERNAL_ACCOUNT_TYPE) { throw new Error( `Expected "${EXTERNAL_ACCOUNT_TYPE}" type but ` + `received "${options.type}"` ); } - this.clientAuth = options.client_id - ? ({ - confidentialClientType: 'basic', - clientId: options.client_id, - clientSecret: options.client_secret, - } as ClientAuthentication) - : undefined; - this.stsCredential = new sts.StsCredentials( - options.token_url, - this.clientAuth + + const clientId = opts.get('client_id'); + const clientSecret = opts.get('client_secret'); + const tokenUrl = opts.get('token_url'); + const subjectTokenType = opts.get('subject_token_type'); + const workforcePoolUserProject = opts.get('workforce_pool_user_project'); + const serviceAccountImpersonationUrl = opts.get( + 'service_account_impersonation_url' + ); + const serviceAccountImpersonation = opts.get( + 'service_account_impersonation' ); + const serviceAccountImpersonationLifetime = originalOrCamelOptions( + serviceAccountImpersonation + ).get('token_lifetime_seconds'); + + if (clientId) { + this.clientAuth = { + confidentialClientType: 'basic', + clientId, + clientSecret, + }; + } + + this.stsCredential = new sts.StsCredentials(tokenUrl, this.clientAuth); // Default OAuth scope. This could be overridden via public property. this.scopes = [DEFAULT_OAUTH_SCOPE]; this.cachedAccessToken = null; - this.audience = options.audience; - this.subjectTokenType = options.subject_token_type; - this.quotaProjectId = options.quota_project_id; - this.workforcePoolUserProject = options.workforce_pool_user_project; + this.audience = opts.get('audience'); + this.subjectTokenType = subjectTokenType; + this.workforcePoolUserProject = workforcePoolUserProject; const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN); if ( this.workforcePoolUserProject && @@ -206,33 +220,18 @@ export abstract class BaseExternalAccountClient extends AuthClient { 'credentials.' ); } - this.serviceAccountImpersonationUrl = - options.service_account_impersonation_url; - + this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl; this.serviceAccountImpersonationLifetime = - options.service_account_impersonation?.token_lifetime_seconds; + serviceAccountImpersonationLifetime; + if (this.serviceAccountImpersonationLifetime) { this.configLifetimeRequested = true; } else { this.configLifetimeRequested = false; this.serviceAccountImpersonationLifetime = DEFAULT_TOKEN_LIFESPAN; } - // As threshold could be zero, - // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the - // zero value. - if (typeof additionalOptions?.eagerRefreshThresholdMillis !== 'number') { - this.eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET; - } else { - this.eagerRefreshThresholdMillis = additionalOptions! - .eagerRefreshThresholdMillis as number; - } - this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; - this.projectId = null; - this.projectNumber = this.getProjectNumber(this.audience); - if (options.universe_domain) { - this.universeDomain = options.universe_domain; - } + this.projectNumber = this.getProjectNumber(this.audience); } /** The service account email to be impersonated, if available. */ diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 29fabd16..ccecb66a 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -16,9 +16,13 @@ import {GaxiosError} from 'gaxios'; import * as gcpMetadata from 'gcp-metadata'; import {CredentialRequest, Credentials} from './credentials'; -import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; +import { + GetTokenResponse, + OAuth2Client, + OAuth2ClientOptions, +} from './oauth2client'; -export interface ComputeOptions extends RefreshOptions { +export interface ComputeOptions extends OAuth2ClientOptions { /** * The service account email to use, or 'default'. A Compute Engine instance * may have multiple service accounts. diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 59305d27..92238b40 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -22,9 +22,9 @@ import * as stream from 'stream'; import {BodyResponseCallback} from '../transporters'; import {Credentials} from './credentials'; -import {AuthClient} from './authclient'; +import {AuthClient, AuthClientOptions} from './authclient'; -import {GetAccessTokenResponse, Headers, RefreshOptions} from './oauth2client'; +import {GetAccessTokenResponse, Headers} from './oauth2client'; import * as sts from './stscredentials'; /** @@ -107,8 +107,6 @@ interface AvailabilityCondition { export class DownscopedClient extends AuthClient { private cachedDownscopedAccessToken: CredentialsWithResponse | null; private readonly stsCredential: sts.StsCredentials; - public readonly eagerRefreshThresholdMillis: number; - public readonly forceRefreshOnFailure: boolean; /** * Instantiates a downscoped client object using the provided source @@ -125,19 +123,18 @@ export class DownscopedClient extends AuthClient { * on the resource that the rule applies to, the upper bound of the * permissions that are available on that resource and an optional * condition to further restrict permissions. - * @param additionalOptions Optional additional behavior customization - * options. These currently customize expiration threshold time and - * whether to retry on 401/403 API request errors. - * @param quotaProjectId Optional quota project id for setting up in the - * x-goog-user-project header. + * @param additionalOptions **DEPRECATED, set this in the provided `authClient`.** + * Optional additional behavior customization options. + * @param quotaProjectId **DEPRECATED, set this in the provided `authClient`.** + * Optional quota project id for setting up in the x-goog-user-project header. */ constructor( private readonly authClient: AuthClient, private readonly credentialAccessBoundary: CredentialAccessBoundary, - additionalOptions?: RefreshOptions, + additionalOptions?: AuthClientOptions, quotaProjectId?: string ) { - super(); + super({...additionalOptions, quotaProjectId}); // Check 1-10 Access Boundary Rules are defined within Credential Access // Boundary. if ( @@ -167,17 +164,6 @@ export class DownscopedClient extends AuthClient { this.stsCredential = new sts.StsCredentials(STS_ACCESS_TOKEN_URL); this.cachedDownscopedAccessToken = null; - // As threshold could be zero, - // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the - // zero value. - if (typeof additionalOptions?.eagerRefreshThresholdMillis !== 'number') { - this.eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET; - } else { - this.eagerRefreshThresholdMillis = additionalOptions! - .eagerRefreshThresholdMillis as number; - } - this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; - this.quotaProjectId = quotaProjectId; } /** diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index e6eca331..c9534440 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {AuthClient} from './authclient'; -import {Headers, RefreshOptions} from './oauth2client'; +import {AuthClient, AuthClientOptions} from './authclient'; +import {Headers} from './oauth2client'; import { ClientAuthentication, getErrorFromOAuthErrorResponse, @@ -30,7 +30,6 @@ import { import {Credentials} from './credentials'; import * as stream from 'stream'; import { - DEFAULT_UNIVERSE, EXPIRATION_TIME_OFFSET, SharedExternalAccountClientOptions, } from './baseexternalclient'; @@ -155,7 +154,6 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { private cachedAccessToken: CredentialsWithResponse | null; private readonly externalAccountAuthorizedUserHandler: ExternalAccountAuthorizedUserHandler; private refreshToken: string; - public universeDomain = DEFAULT_UNIVERSE; /** * Instantiates an ExternalAccountAuthorizedUserClient instances using the @@ -163,15 +161,16 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { * An error is throws if the credential is not valid. * @param options The external account authorized user option object typically * from the external accoutn authorized user JSON credential file. - * @param additionalOptions Optional additional behavior customization - * options. These currently customize expiration threshold time and - * whether to retry on 401/403 API request errors. + * @param additionalOptions **DEPRECATED, all options are available in the + * `options` parameter.** Optional additional behavior customization options. + * These currently customize expiration threshold time and whether to retry + * on 401/403 API request errors. */ constructor( options: ExternalAccountAuthorizedUserClientOptions, - additionalOptions?: RefreshOptions + additionalOptions?: AuthClientOptions ) { - super(); + super({...options, ...additionalOptions}); this.refreshToken = options.refresh_token; const clientAuth = { confidentialClientType: 'basic', diff --git a/src/auth/externalclient.ts b/src/auth/externalclient.ts index b9c792ff..95b97da3 100644 --- a/src/auth/externalclient.ts +++ b/src/auth/externalclient.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {RefreshOptions} from './oauth2client'; import { BaseExternalAccountClient, // This is the identifier in the JSON config for the type of credential. @@ -33,6 +32,7 @@ import { PluggableAuthClient, PluggableAuthClientOptions, } from './pluggable-auth-client'; +import {AuthClientOptions} from './authclient'; export type ExternalAccountClientOptions = | IdentityPoolClientOptions @@ -60,15 +60,16 @@ export class ExternalAccountClient { * underlying credential source. * @param options The external account options object typically loaded * from the external account JSON credential file. - * @param additionalOptions Optional additional behavior customization - * options. These currently customize expiration threshold time and - * whether to retry on 401/403 API request errors. + * @param additionalOptions **DEPRECATED, all options are available in the + * `options` parameter.** Optional additional behavior customization options. + * These currently customize expiration threshold time and whether to retry + * on 401/403 API request errors. * @return A BaseExternalAccountClient instance or null if the options * provided do not correspond to an external account credential. */ static fromJSON( options: ExternalAccountClientOptions, - additionalOptions?: RefreshOptions + additionalOptions?: AuthClientOptions ): BaseExternalAccountClient | null { if (options && options.type === EXTERNAL_ACCOUNT_TYPE) { if ((options as AwsClientOptions).credential_source?.environment_id) { diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 7167c416..647090c1 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -28,7 +28,7 @@ import {CredentialBody, ImpersonatedJWTInput, JWTInput} from './credentials'; import {IdTokenClient} from './idtokenclient'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; -import {Headers, OAuth2ClientOptions, RefreshOptions} from './oauth2client'; +import {Headers, OAuth2ClientOptions} from './oauth2client'; import { UserRefreshClient, UserRefreshClientOptions, @@ -47,7 +47,7 @@ import { EXTERNAL_ACCOUNT_TYPE, BaseExternalAccountClient, } from './baseexternalclient'; -import {AuthClient} from './authclient'; +import {AuthClient, AuthClientOptions} from './authclient'; import { EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, ExternalAccountAuthorizedUserClient, @@ -166,7 +166,7 @@ export class GoogleAuth { defaultScopes?: string | string[]; private keyFilename?: string; private scopes?: string | string[]; - private clientOptions?: RefreshOptions; + private clientOptions?: AuthClientOptions; /** * Export DefaultTransporter as a static property of the class. @@ -302,13 +302,16 @@ export class GoogleAuth { */ getApplicationDefault(): Promise; getApplicationDefault(callback: ADCCallback): void; - getApplicationDefault(options: RefreshOptions): Promise; - getApplicationDefault(options: RefreshOptions, callback: ADCCallback): void; + getApplicationDefault(options: AuthClientOptions): Promise; getApplicationDefault( - optionsOrCallback: ADCCallback | RefreshOptions = {}, + options: AuthClientOptions, + callback: ADCCallback + ): void; + getApplicationDefault( + optionsOrCallback: ADCCallback | AuthClientOptions = {}, callback?: ADCCallback ): void | Promise { - let options: RefreshOptions | undefined; + let options: AuthClientOptions | undefined; if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; } else { @@ -325,7 +328,7 @@ export class GoogleAuth { } private async getApplicationDefaultAsync( - options: RefreshOptions = {} + options: AuthClientOptions = {} ): Promise { // If we've already got a cached credential, return it. // This will also preserve one's configured quota project, in case they @@ -432,7 +435,7 @@ export class GoogleAuth { * @api private */ async _tryGetApplicationCredentialsFromEnvironmentVariable( - options?: RefreshOptions + options?: AuthClientOptions ): Promise { const credentialsPath = process.env['GOOGLE_APPLICATION_CREDENTIALS'] || @@ -460,7 +463,7 @@ export class GoogleAuth { * @api private */ async _tryGetApplicationCredentialsFromWellKnownFile( - options?: RefreshOptions + options?: AuthClientOptions ): Promise { // First, figure out the location of the file, depending upon the OS type. let location = null; @@ -505,7 +508,7 @@ export class GoogleAuth { */ async _getApplicationCredentialsFromFilePath( filePath: string, - options: RefreshOptions = {} + options: AuthClientOptions = {} ): Promise { // Make sure the path looks like a string. if (!filePath || filePath.length === 0) { @@ -609,11 +612,10 @@ export class GoogleAuth { */ fromJSON( json: JWTInput | ImpersonatedJWTInput, - options: RefreshOptions = {} + options: AuthClientOptions = {} ): JSONClient { let client: JSONClient; - options = options || {}; if (json.type === USER_REFRESH_ACCOUNT_TYPE) { client = new UserRefreshClient(options); client.fromJSON(json); @@ -647,8 +649,8 @@ export class GoogleAuth { * @returns JWT or UserRefresh Client with data */ private _cacheClientFromJSON( - json: JWTInput, - options?: RefreshOptions + json: JWTInput | ImpersonatedJWTInput, + options?: AuthClientOptions ): JSONClient { const client = this.fromJSON(json, options); @@ -667,19 +669,19 @@ export class GoogleAuth { fromStream(inputStream: stream.Readable, callback: CredentialCallback): void; fromStream( inputStream: stream.Readable, - options: RefreshOptions + options: AuthClientOptions ): Promise; fromStream( inputStream: stream.Readable, - options: RefreshOptions, + options: AuthClientOptions, callback: CredentialCallback ): void; fromStream( inputStream: stream.Readable, - optionsOrCallback: RefreshOptions | CredentialCallback = {}, + optionsOrCallback: AuthClientOptions | CredentialCallback = {}, callback?: CredentialCallback ): Promise | void { - let options: RefreshOptions = {}; + let options: AuthClientOptions = {}; if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; } else { @@ -697,7 +699,7 @@ export class GoogleAuth { private fromStreamAsync( inputStream: stream.Readable, - options?: RefreshOptions + options?: AuthClientOptions ): Promise { return new Promise((resolve, reject) => { if (!inputStream) { @@ -741,7 +743,7 @@ export class GoogleAuth { * @param options An optional options object. * @returns A JWT loaded from the key */ - fromAPIKey(apiKey: string, options?: RefreshOptions): JWT { + fromAPIKey(apiKey: string, options?: AuthClientOptions): JWT { options = options || {}; const client = new JWT(options); client.fromAPIKey(apiKey); diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index 06c1e9b5..2ed66b11 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -20,7 +20,8 @@ import { BaseExternalAccountClient, BaseExternalAccountClientOptions, } from './baseexternalclient'; -import {RefreshOptions} from './oauth2client'; +import {AuthClientOptions} from './authclient'; +import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; // fs.readfile is undefined in browser karma tests causing // `npm run browser-test` to fail as test.oauth2.ts imports this file via @@ -73,19 +74,28 @@ export class IdentityPoolClient extends BaseExternalAccountClient { * url-sourced credential or a workforce pool user project is provided * with a non workforce audience. * @param options The external account options object typically loaded - * from the external account JSON credential file. - * @param additionalOptions Optional additional behavior customization - * options. These currently customize expiration threshold time and - * whether to retry on 401/403 API request errors. + * from the external account JSON credential file. The camelCased options + * are aliases for the snake_cased options. + * @param additionalOptions **DEPRECATED, all options are available in the + * `options` parameter.** Optional additional behavior customization options. + * These currently customize expiration threshold time and whether to retry + * on 401/403 API request errors. */ constructor( - options: IdentityPoolClientOptions, - additionalOptions?: RefreshOptions + options: + | IdentityPoolClientOptions + | SnakeToCamelObject, + additionalOptions?: AuthClientOptions ) { super(options, additionalOptions); - this.file = options.credential_source.file; - this.url = options.credential_source.url; - this.headers = options.credential_source.headers; + + const opts = originalOrCamelOptions(options as IdentityPoolClientOptions); + const credentialSource = opts.get('credential_source'); + const credentialSourceOpts = originalOrCamelOptions(credentialSource); + + this.file = credentialSourceOpts.get('file'); + this.url = credentialSourceOpts.get('url'); + this.headers = credentialSourceOpts.get('headers'); if (this.file && this.url) { throw new Error( 'No valid Identity Pool "credential_source" provided, must be either file or url.' @@ -99,10 +109,17 @@ export class IdentityPoolClient extends BaseExternalAccountClient { 'No valid Identity Pool "credential_source" provided, must be either file or url.' ); } + + const formatOpts = originalOrCamelOptions( + credentialSourceOpts.get('format') + ); + // Text is the default format type. - this.formatType = options.credential_source.format?.type || 'text'; - this.formatSubjectTokenFieldName = - options.credential_source.format?.subject_token_field_name; + this.formatType = formatOpts.get('type') || 'text'; + this.formatSubjectTokenFieldName = formatOpts.get( + 'subject_token_field_name' + ); + if (this.formatType !== 'json' && this.formatType !== 'text') { throw new Error(`Invalid credential_source format "${this.formatType}"`); } diff --git a/src/auth/idtokenclient.ts b/src/auth/idtokenclient.ts index 38a72278..03e186ac 100644 --- a/src/auth/idtokenclient.ts +++ b/src/auth/idtokenclient.ts @@ -13,9 +13,14 @@ // limitations under the License. import {Credentials} from './credentials'; -import {Headers, OAuth2Client, RequestMetadataResponse} from './oauth2client'; +import { + Headers, + OAuth2Client, + OAuth2ClientOptions, + RequestMetadataResponse, +} from './oauth2client'; -export interface IdTokenOptions { +export interface IdTokenOptions extends OAuth2ClientOptions { /** * The client to make the request to fetch an ID token. */ @@ -41,7 +46,7 @@ export class IdTokenClient extends OAuth2Client { * See: https://developers.google.com/compute/docs/authentication */ constructor(options: IdTokenOptions) { - super(); + super(options); this.targetAudience = options.targetAudience; this.idTokenProvider = options.idTokenProvider; } diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index b2cb6994..24debd81 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -14,12 +14,16 @@ * limitations under the License. */ -import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; +import { + GetTokenResponse, + OAuth2Client, + OAuth2ClientOptions, +} from './oauth2client'; import {AuthClient} from './authclient'; import {IdTokenProvider} from './idtokenclient'; import {GaxiosError} from 'gaxios'; -export interface ImpersonatedOptions extends RefreshOptions { +export interface ImpersonatedOptions extends OAuth2ClientOptions { /** * Client used to perform exchange for impersonated client. */ @@ -108,6 +112,8 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { */ constructor(options: ImpersonatedOptions = {}) { super(options); + // Start with an expired refresh token, which will automatically be + // refreshed before the first API call is made. this.credentials = { expiry_date: 1, refresh_token: 'impersonated-placeholder', diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 0d851303..d25f4146 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -21,11 +21,11 @@ import {JWTAccess} from './jwtaccess'; import { GetTokenResponse, OAuth2Client, - RefreshOptions, + OAuth2ClientOptions, RequestMetadataResponse, } from './oauth2client'; -export interface JWTOptions extends RefreshOptions { +export interface JWTOptions extends OAuth2ClientOptions { email?: string; keyFile?: string; key?: string; @@ -83,10 +83,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { optionsOrEmail && typeof optionsOrEmail === 'object' ? optionsOrEmail : {email: optionsOrEmail, keyFile, key, keyId, scopes, subject}; - super({ - eagerRefreshThresholdMillis: opts.eagerRefreshThresholdMillis, - forceRefreshOnFailure: opts.forceRefreshOnFailure, - }); + super(opts); this.email = opts.email; this.keyFile = opts.keyFile; this.key = opts.key; @@ -94,6 +91,8 @@ export class JWT extends OAuth2Client implements IdTokenProvider { this.scopes = opts.scopes; this.subject = opts.subject; this.additionalClaims = opts.additionalClaims; + // Start with an expired refresh token, which will automatically be + // refreshed before the first API call is made. this.credentials = {refresh_token: 'jwt-placeholder', expiry_date: 1}; } @@ -103,15 +102,10 @@ export class JWT extends OAuth2Client implements IdTokenProvider { * @return The cloned instance. */ createScoped(scopes?: string | string[]) { - return new JWT({ - email: this.email, - keyFile: this.keyFile, - key: this.key, - keyId: this.keyId, - scopes, - subject: this.subject, - additionalClaims: this.additionalClaims, - }); + const jwt = new JWT(this as {} as JWTOptions); + jwt.scopes = scopes; + + return jwt; } /** diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index fc0fddc7..c5ecf5b2 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -25,7 +25,7 @@ import * as formatEcdsa from 'ecdsa-sig-formatter'; import {createCrypto, JwkCertificate, hasBrowserCrypto} from '../crypto/crypto'; import {BodyResponseCallback} from '../transporters'; -import {AuthClient} from './authclient'; +import {AuthClient, AuthClientOptions} from './authclient'; import {CredentialRequest, Credentials} from './credentials'; import {LoginTicket, TokenPayload} from './loginticket'; /** @@ -404,26 +404,18 @@ export interface VerifyIdTokenOptions { maxExpiry?: number; } -export interface RefreshOptions { - // Eagerly refresh unexpired tokens when they are within this many - // milliseconds from expiring". - // Defaults to a value of 300000 (5 minutes). - eagerRefreshThresholdMillis?: number; - - // Whether to attempt to lazily refresh tokens on 401/403 responses - // even if an attempt is made to refresh the token preemptively based - // on the expiry_date. - // Defaults to false. - forceRefreshOnFailure?: boolean; -} - -export interface OAuth2ClientOptions extends RefreshOptions { +export interface OAuth2ClientOptions extends AuthClientOptions { clientId?: string; clientSecret?: string; redirectUri?: string; - credentials?: Credentials; } +// Re-exporting here for backwards compatibility +export type RefreshOptions = Pick< + AuthClientOptions, + 'eagerRefreshThresholdMillis' | 'forceRefreshOnFailure' +>; + export class OAuth2Client extends AuthClient { private redirectUri?: string; private certificateCache: Certificates = {}; @@ -439,12 +431,6 @@ export class OAuth2Client extends AuthClient { apiKey?: string; - projectId?: string; - - eagerRefreshThresholdMillis: number; - - forceRefreshOnFailure: boolean; - refreshHandler?: GetRefreshHandlerCallback; /** @@ -464,18 +450,16 @@ export class OAuth2Client extends AuthClient { clientSecret?: string, redirectUri?: string ) { - super(); const opts = optionsOrClientId && typeof optionsOrClientId === 'object' ? optionsOrClientId : {clientId: optionsOrClientId, clientSecret, redirectUri}; + + super(opts); + this._clientId = opts.clientId; this._clientSecret = opts.clientSecret; this.redirectUri = opts.redirectUri; - this.eagerRefreshThresholdMillis = - opts.eagerRefreshThresholdMillis || 5 * 60 * 1000; - this.forceRefreshOnFailure = !!opts.forceRefreshOnFailure; - this.credentials = opts.credentials || {}; } protected static readonly GOOGLE_TOKEN_INFO_URL = diff --git a/src/auth/pluggable-auth-client.ts b/src/auth/pluggable-auth-client.ts index ad31c78a..ad9a933f 100644 --- a/src/auth/pluggable-auth-client.ts +++ b/src/auth/pluggable-auth-client.ts @@ -16,12 +16,12 @@ import { BaseExternalAccountClient, BaseExternalAccountClientOptions, } from './baseexternalclient'; -import {RefreshOptions} from './oauth2client'; import { ExecutableResponse, InvalidExpirationTimeFieldError, } from './executable-response'; import {PluggableAuthHandler} from './pluggable-auth-handler'; +import {AuthClientOptions} from './authclient'; /** * Defines the credential source portion of the configuration for PluggableAuthClient. @@ -189,13 +189,14 @@ export class PluggableAuthClient extends BaseExternalAccountClient { * An error is thrown if the credential is not a valid pluggable auth credential. * @param options The external account options object typically loaded from * the external account JSON credential file. - * @param additionalOptions Optional additional behavior customization - * options. These currently customize expiration threshold time and - * whether to retry on 401/403 API request errors. + * @param additionalOptions **DEPRECATED, all options are available in the + * `options` parameter.** Optional additional behavior customization options. + * These currently customize expiration threshold time and whether to retry + * on 401/403 API request errors. */ constructor( options: PluggableAuthClientOptions, - additionalOptions?: RefreshOptions + additionalOptions?: AuthClientOptions ) { super(options, additionalOptions); if (!options.credential_source.executable) { diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index e7ca73b3..816be6e5 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -14,11 +14,15 @@ import * as stream from 'stream'; import {JWTInput} from './credentials'; -import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; +import { + GetTokenResponse, + OAuth2Client, + OAuth2ClientOptions, +} from './oauth2client'; export const USER_REFRESH_ACCOUNT_TYPE = 'authorized_user'; -export interface UserRefreshClientOptions extends RefreshOptions { +export interface UserRefreshClientOptions extends OAuth2ClientOptions { clientId?: string; clientSecret?: string; refreshToken?: string; @@ -57,12 +61,7 @@ export class UserRefreshClient extends OAuth2Client { eagerRefreshThresholdMillis, forceRefreshOnFailure, }; - super({ - clientId: opts.clientId, - clientSecret: opts.clientSecret, - eagerRefreshThresholdMillis: opts.eagerRefreshThresholdMillis, - forceRefreshOnFailure: opts.forceRefreshOnFailure, - }); + super(opts); this._refreshToken = opts.refreshToken; this.credentials.refresh_token = opts.refreshToken; } diff --git a/src/transporters.ts b/src/transporters.ts index 26c1dab2..d4a298f0 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -13,11 +13,11 @@ // limitations under the License. import { + Gaxios, GaxiosError, GaxiosOptions, GaxiosPromise, GaxiosResponse, - request, } from 'gaxios'; import {validate} from './options'; @@ -27,6 +27,7 @@ const pkg = require('../../package.json'); const PRODUCT_NAME = 'google-api-nodejs-client'; export interface Transporter { + defaults?: GaxiosOptions; request(opts: GaxiosOptions): GaxiosPromise; } @@ -45,6 +46,11 @@ export class DefaultTransporter implements Transporter { */ static readonly USER_AGENT = `${PRODUCT_NAME}/${pkg.version}`; + /** + * A configurable, replacable `Gaxios` instance. + */ + instance = new Gaxios(); + /** * Configures request options before making a request. * @param opts GaxiosOptions options. @@ -81,11 +87,19 @@ export class DefaultTransporter implements Transporter { // ensure the user isn't passing in request-style options opts = this.configure(opts); validate(opts); - return request(opts).catch(e => { + return this.instance.request(opts).catch(e => { throw this.processError(e); }); } + get defaults() { + return this.instance.defaults; + } + + set defaults(opts: GaxiosOptions) { + this.instance.defaults = opts; + } + /** * Changes the error to include details from the body. */ diff --git a/src/util.ts b/src/util.ts index a9cd68b3..ed4b92de 100644 --- a/src/util.ts +++ b/src/util.ts @@ -12,6 +12,146 @@ // See the License for the specific language governing permissions and // limitations under the License. +/** + * A utility for converting snake_case to camelCase. + * + * For, for example `my_snake_string` becomes `mySnakeString`. + * + * @internal + */ +export type SnakeToCamel = S extends `${infer FirstWord}_${infer Remainder}` + ? `${FirstWord}${Capitalize>}` + : S; + +/** + * A utility for converting an type's keys from snake_case + * to camelCase, if the keys are strings. + * + * For example: + * + * ```ts + * { + * my_snake_string: boolean; + * myCamelString: string; + * my_snake_obj: { + * my_snake_obj_string: string; + * }; + * } + * ``` + * + * becomes: + * + * ```ts + * { + * mySnakeString: boolean; + * myCamelString: string; + * mySnakeObj: { + * mySnakeObjString: string; + * } + * } + * ``` + * + * @remarks + * + * The generated documentation for the camelCase'd properties won't be available + * until {@link https://github.com/microsoft/TypeScript/issues/50715} has been + * resolved. + * + * @internal + */ +export type SnakeToCamelObject = { + [K in keyof T as SnakeToCamel]: T[K] extends {} + ? SnakeToCamelObject + : T[K]; +}; + +/** + * A utility for adding camelCase versions of a type's snake_case keys, if the + * keys are strings, preserving any existing keys. + * + * For example: + * + * ```ts + * { + * my_snake_boolean: boolean; + * myCamelString: string; + * my_snake_obj: { + * my_snake_obj_string: string; + * }; + * } + * ``` + * + * becomes: + * + * ```ts + * { + * my_snake_boolean: boolean; + * mySnakeBoolean: boolean; + * myCamelString: string; + * my_snake_obj: { + * my_snake_obj_string: string; + * }; + * mySnakeObj: { + * mySnakeObjString: string; + * } + * } + * ``` + * @remarks + * + * The generated documentation for the camelCase'd properties won't be available + * until {@link https://github.com/microsoft/TypeScript/issues/50715} has been + * resolved. + * + * Tracking: {@link https://github.com/googleapis/google-auth-library-nodejs/issues/1686} + * + * @internal + */ +export type OriginalAndCamel = { + [K in keyof T as K | SnakeToCamel]: T[K] extends {} + ? OriginalAndCamel + : T[K]; +}; + +/** + * Returns the camel case of a provided string. + * + * @remarks + * + * Match any `_` and not `_` pair, then return the uppercase of the not `_` + * character. + * + * @internal + * + * @param str the string to convert + * @returns the camelCase'd string + */ +export function snakeToCamel(str: T): SnakeToCamel { + return str.replace(/([_][^_])/g, match => + match.slice(1).toUpperCase() + ) as SnakeToCamel; +} + +/** + * Get the value of `obj[key]` or `obj[camelCaseKey]`, with a preference + * for original, non-camelCase key. + * + * @param obj object to lookup a value in + * @returns a `get` function for getting `obj[key || snakeKey]`, if available + */ +export function originalOrCamelOptions(obj?: T) { + /** + * + * @param key an index of object, preferably snake_case + * @returns the value `obj[key || snakeKey]`, if available + */ + function get & string>(key: K) { + const o = (obj || {}) as OriginalAndCamel; + return o[key] ?? o[snakeToCamel(key) as K]; + } + + return {get}; +} + export interface LRUCacheOptions { /** * The maximum number of items to cache. diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index 79308005..2c149633 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/node": "^16.11.3", - "typescript": "^3.0.0", + "typescript": "^5.0.0", "gts": "^5.0.0", "null-loader": "^4.0.0", "ts-loader": "^8.0.0", diff --git a/test/test.authclient.ts b/test/test.authclient.ts new file mode 100644 index 00000000..786d4d04 --- /dev/null +++ b/test/test.authclient.ts @@ -0,0 +1,60 @@ +// Copyright 2023 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. + +import {strict as assert} from 'assert'; + +import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; +import {AuthClient} from '../src'; +import {Headers} from '../src/auth/oauth2client'; +import {snakeToCamel} from '../src/util'; + +describe('AuthClient', () => { + class TestAuthClient extends AuthClient { + request(opts: GaxiosOptions): GaxiosPromise { + throw new Error('Method not implemented.'); + } + + getRequestHeaders(url?: string | undefined): Promise { + throw new Error('Method not implemented.'); + } + + getAccessToken(): Promise<{ + token?: string | null | undefined; + res?: GaxiosResponse | null | undefined; + }> { + throw new Error('Method not implemented.'); + } + } + + it('should accept and normalize snake case options to camel case', () => { + const expected = { + project_id: 'my-projectId', + quota_project_id: 'my-quota-project-id', + credentials: {}, + universe_domain: 'my-universe-domain', + }; + + for (const [key, value] of Object.entries(expected)) { + const camelCased = snakeToCamel(key) as keyof typeof authClient; + + // assert snake cased input + let authClient = new TestAuthClient({[key]: value}); + assert.equal(authClient[camelCased], value); + + // assert camel cased input + authClient = new TestAuthClient({[camelCased]: value}); + assert.equal(authClient[camelCased], value); + } + }); +}); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 9d57c74b..96ea57ce 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -40,7 +40,7 @@ import { mockStsTokenExchange, getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; -import {RefreshOptions} from '../src'; +import {AuthClientOptions} from '../src/auth/authclient'; nock.disableNetConnect(); @@ -55,7 +55,7 @@ class TestExternalAccountClient extends BaseExternalAccountClient { constructor( options: BaseExternalAccountClientOptions, - additionalOptions?: RefreshOptions + additionalOptions?: Partial ) { super(options, additionalOptions); this.credentialSourceType = 'test'; diff --git a/test/test.util.ts b/test/test.util.ts index 6871c621..6f584c9c 100644 --- a/test/test.util.ts +++ b/test/test.util.ts @@ -13,10 +13,21 @@ // limitations under the License. import {strict as assert} from 'assert'; +import * as sinon from 'sinon'; import {LRUCache} from '../src/util'; describe('util', () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + describe('LRUCache', () => { it('should set and get a cached item', () => { const expected = 'value'; @@ -50,18 +61,23 @@ describe('util', () => { it('should evict items older than a supplied `maxAge`', async () => { const maxAge = 50; + sandbox.clock = sinon.useFakeTimers(); + const lru = new LRUCache({capacity: 5, maxAge}); lru.set('first', 1); lru.set('second', 2); - await new Promise(res => setTimeout(res, maxAge + 1)); + // back to the future 🏎️ + sandbox.clock.tick(maxAge + 1); + // just set, so should be fine lru.set('third', 3); + assert.equal(lru.get('third'), 3); + // these are too old assert.equal(lru.get('first'), undefined); assert.equal(lru.get('second'), undefined); - assert.equal(lru.get('third'), 3); }); }); }); From 903477a375b699ce024a961df9f312aeed1dd403 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 27 Oct 2023 01:04:13 +0200 Subject: [PATCH 473/662] chore(deps): update actions/setup-node action to v4 (#1683) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 01a63060..6290a497 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: node: [14, 16, 18, 20] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: node --version @@ -30,7 +30,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 14 - run: npm install @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 14 - run: npm install From d2a1aa932cece42c0759e92060f338372236a7f2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 27 Oct 2023 01:10:47 +0200 Subject: [PATCH 474/662] chore(deps): update dependency @types/node to v20 (#1684) --- system-test/fixtures/kitchen/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index 2c149633..ecf79ef9 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -17,7 +17,7 @@ "google-auth-library": "file:./google-auth-library.tgz" }, "devDependencies": { - "@types/node": "^16.11.3", + "@types/node": "^20.0.0", "typescript": "^5.0.0", "gts": "^5.0.0", "null-loader": "^4.0.0", From 02e30d4bd781792ee07a345312efe15e689bf134 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 16:16:33 -0700 Subject: [PATCH 475/662] chore(main): release 9.2.0 (#1670) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Daniel Bankhead --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 032a26e9..516bd87c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.2.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.1.0...v9.2.0) (2023-10-26) + + +### Features + +* Unify Base `AuthClient` Options ([#1663](https://github.com/googleapis/google-auth-library-nodejs/issues/1663)) ([5ac6705](https://github.com/googleapis/google-auth-library-nodejs/commit/5ac67052f6c19e93c3e8c4e1636fad4737fcee08)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v12 ([#1671](https://github.com/googleapis/google-auth-library-nodejs/issues/1671)) ([4f94ffe](https://github.com/googleapis/google-auth-library-nodejs/commit/4f94ffe474ee41b560813b81e8d621be848d38d0)) +* DOS security risks ([#1668](https://github.com/googleapis/google-auth-library-nodejs/issues/1668)) ([b25d4f5](https://github.com/googleapis/google-auth-library-nodejs/commit/b25d4f54f88c52c8496ef65f5ab2a75122100f2c)) +* Increase max asset size ([#1682](https://github.com/googleapis/google-auth-library-nodejs/issues/1682)) ([edb9401](https://github.com/googleapis/google-auth-library-nodejs/commit/edb94018cadd2ad796ccec5496e3bfcbade39c7f)) +* Remove broken source maps ([#1669](https://github.com/googleapis/google-auth-library-nodejs/issues/1669)) ([56cb3ad](https://github.com/googleapis/google-auth-library-nodejs/commit/56cb3ad24c5e4b6952169b88d0a43e89cbf1252e)) + ## [9.1.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.0.0...v9.1.0) (2023-10-02) diff --git a/package.json b/package.json index 0ca5eafd..97c701ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.1.0", + "version": "9.2.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index c732af44..a313b6b4 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^12.0.0", - "google-auth-library": "^9.1.0", + "google-auth-library": "^9.2.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From d223a36d5bc9cd4487ce81167e8d7030e32836ea Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:11:30 -0800 Subject: [PATCH 476/662] chore: update cloud-rad version to ^0.4.0 (#1691) Source-Link: https://github.com/googleapis/synthtool/commit/1063ef32bfe41b112bade7a2dfad4e84d0058ebd Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:e92044720ab3cb6984a70b0c6001081204375959ba3599ef6c42dd99a7783a67 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/release/docs-devsite.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 40b49d2b..638efabf 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:abc68a9bbf4fa808b25fa16d3b11141059dc757dbc34f024744bba36c200b40f -# created: 2023-10-04T20:56:40.710775365Z + digest: sha256:e92044720ab3cb6984a70b0c6001081204375959ba3599ef6c42dd99a7783a67 +# created: 2023-11-10T00:24:05.581078808Z diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index 3596c1e4..81a89f6c 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -25,6 +25,6 @@ if [[ -z "$CREDENTIALS" ]]; then fi npm install -npm install --no-save @google-cloud/cloud-rad@^0.3.7 +npm install --no-save @google-cloud/cloud-rad@^0.4.0 # publish docs to devsite npx @google-cloud/cloud-rad . cloud-rad From bf219c840a7a1aa391b2f0f71eed62acd8eee936 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 15 Nov 2023 10:54:15 -0800 Subject: [PATCH 477/662] test: Remove Long-Skipped Tests (#1693) --- samples/test/jwt.test.js | 6 ------ test/test.transporters.ts | 20 -------------------- 2 files changed, 26 deletions(-) diff --git a/samples/test/jwt.test.js b/samples/test/jwt.test.js index e87792bd..5ad080ec 100644 --- a/samples/test/jwt.test.js +++ b/samples/test/jwt.test.js @@ -31,12 +31,6 @@ describe('samples', () => { assert.match(output, /DNS Info:/); }); - it.skip('should acquire compute credentials', async () => { - // TODO: need to figure out deploying to GCF for this to work - const output = execSync('node compute'); - assert.match(output, /DNS Info:/); - }); - it('should create a JWT', async () => { const output = execSync('node jwt'); assert.match(output, /DNS Info:/); diff --git a/test/test.transporters.ts b/test/test.transporters.ts index c1949bc5..d7da2196 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -161,24 +161,4 @@ describe('transporters', () => { } ); }); - - // tslint:disable-next-line ban - it.skip('should use the https proxy if one is configured', async () => { - process.env['https_proxy'] = 'https://han:solo@proxy-server:1234'; - const transporter = new DefaultTransporter(); - const scope = nock('https://proxy-server:1234') - .get('https://example.com/fake', undefined, { - reqheaders: { - host: 'example.com', - accept: /.*/g, - 'user-agent': /google-api-nodejs-client\/.*/g, - 'proxy-authorization': /.*/g, - }, - }) - .reply(200); - const url = 'https://example.com/fake'; - const result = await transporter.request({url}); - scope.done(); - assert.strictEqual(result.status, 200); - }); }); From a735ec5f5e280a1c47f0f4d5e99acf228d5a320a Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 28 Nov 2023 17:36:11 -0800 Subject: [PATCH 478/662] feat: Retrieve `universe_domain` for Compute clients (#1692) * feat: Get `universe_domain` for Compute clients * refactor: streamline * refactor: Fallback to returning default universe * fix: transparent error message * chore: cleanup * docs: Minor doc change --- src/auth/credentials.ts | 2 + src/auth/googleauth.ts | 117 ++++++++++++++++++++++++++-------------- test/test.googleauth.ts | 67 +++++++++++++---------- 3 files changed, 118 insertions(+), 68 deletions(-) diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index ce149b63..f0d3876f 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -76,6 +76,7 @@ export interface JWTInput { client_secret?: string; refresh_token?: string; quota_project_id?: string; + universe_domain?: string; } export interface ImpersonatedJWTInput { @@ -88,4 +89,5 @@ export interface ImpersonatedJWTInput { export interface CredentialBody { client_email?: string; private_key?: string; + universe_domain?: string; } diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 647090c1..13ab18a4 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -14,7 +14,7 @@ import {exec} from 'child_process'; import * as fs from 'fs'; -import {GaxiosOptions, GaxiosResponse} from 'gaxios'; +import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; import * as gcpMetadata from 'gcp-metadata'; import * as os from 'os'; import * as path from 'path'; @@ -47,12 +47,13 @@ import { EXTERNAL_ACCOUNT_TYPE, BaseExternalAccountClient, } from './baseexternalclient'; -import {AuthClient, AuthClientOptions} from './authclient'; +import {AuthClient, AuthClientOptions, DEFAULT_UNIVERSE} from './authclient'; import { EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, ExternalAccountAuthorizedUserClient, ExternalAccountAuthorizedUserClientOptions, } from './externalAccountAuthorizedUserClient'; +import {originalOrCamelOptions} from '../util'; /** * Defines all types of explicit clients that are determined via ADC JSON @@ -131,6 +132,14 @@ const GoogleAuthExceptionMessages = { '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', + NO_CREDENTIALS_FOUND: + 'Unable to find credentials in current environment. \n' + + 'To learn more about authentication and Google APIs, visit: \n' + + 'https://cloud.google.com/docs/authentication/getting-started', + NO_UNIVERSE_DOMAIN_FOUND: + 'Unable to detect a Universe Domain in the current environment.\n' + + 'To learn more about Universe Domain retrieval, visit: \n' + + 'https://cloud.google.com/compute/docs/metadata/predefined-metadata-keys', } as const; export class GoogleAuth { @@ -168,6 +177,13 @@ export class GoogleAuth { private scopes?: string | string[]; private clientOptions?: AuthClientOptions; + /** + * The cached universe domain. + * + * @see {@link GoogleAuth.getUniverseDomain} + */ + #universeDomain?: string = undefined; + /** * Export DefaultTransporter as a static property of the class. */ @@ -286,6 +302,42 @@ export class GoogleAuth { return this._findProjectIdPromise; } + async #getUniverseFromMetadataServer() { + if (!(await this._checkIsGCE())) return; + + let universeDomain: string; + + try { + universeDomain = await gcpMetadata.universe('universe_domain'); + universeDomain ||= DEFAULT_UNIVERSE; + } catch (e) { + if (e instanceof GaxiosError && e.status === 404) { + universeDomain = DEFAULT_UNIVERSE; + } else { + throw e; + } + } + + return universeDomain; + } + + /** + * Retrieves, caches, and returns the universe domain in the following order + * of precedence: + * - The universe domain in {@link GoogleAuth.clientOptions} + * - {@link gcpMetadata.universe} + * + * @returns The universe domain + */ + async getUniverseDomain(): Promise { + this.#universeDomain ??= originalOrCamelOptions(this.clientOptions).get( + 'universe_domain' + ); + this.#universeDomain ??= await this.#getUniverseFromMetadataServer(); + + return this.#universeDomain || DEFAULT_UNIVERSE; + } + /** * @returns Any scopes (user-specified or default scopes specified by the * client library) that need to be set on the current Auth client. @@ -370,30 +422,21 @@ export class GoogleAuth { } // 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}`; + if (await this._checkIsGCE()) { + // set universe domain for Compute client + if (!originalOrCamelOptions(options).get('universe_domain')) { + options.universeDomain = await this.getUniverseDomain(); } - 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.' + (options as ComputeOptions).scopes = this.getAnyScopes(); + return await this.prepareAndCacheADC( + new Compute(options), + quotaProjectIdOverride ); } - // For GCE, just return a default ComputeClient. It will take care of - // the rest. - (options as ComputeOptions).scopes = this.getAnyScopes(); - return await this.prepareAndCacheADC( - new Compute(options), - quotaProjectIdOverride + throw new Error( + 'Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.' ); } @@ -893,37 +936,31 @@ export class GoogleAuth { if (client instanceof BaseExternalAccountClient) { const serviceAccountEmail = client.getServiceAccountEmail(); if (serviceAccountEmail) { - return {client_email: serviceAccountEmail}; + return { + client_email: serviceAccountEmail, + universe_domain: client.universeDomain, + }; } } if (this.jsonContent) { - const credential: CredentialBody = { + return { client_email: (this.jsonContent as JWTInput).client_email, private_key: (this.jsonContent as JWTInput).private_key, + universe_domain: this.jsonContent.universe_domain, }; - 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 (await this._checkIsGCE()) { + const [client_email, universe_domain] = await Promise.all([ + gcpMetadata.instance('service-accounts/default/email'), + this.getUniverseDomain(), + ]); - if (!data || !data.default || !data.default.email) { - throw new Error('Failure from metadata server.'); + return {client_email, universe_domain}; } - return {client_email: data.default.email}; + throw new Error(GoogleAuthExceptionMessages.NO_CREDENTIALS_FOUND); } /** diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index e22556d8..6ba7f4e9 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -64,7 +64,8 @@ describe('googleauth', () => { const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`; const host = HOST_ADDRESS; const instancePath = `${BASE_PATH}/instance`; - const svcAccountPath = `${instancePath}/service-accounts/?recursive=true`; + const svcAccountPath = `${instancePath}/service-accounts/default/email`; + const universeDomainPath = `${BASE_PATH}/universe/universe_domain`; const API_KEY = 'test-123'; const PEM_PATH = './test/fixtures/private.pem'; const STUB_PROJECT = 'my-awesome-project'; @@ -199,20 +200,22 @@ describe('googleauth', () => { createLinuxWellKnownStream = () => fs.createReadStream(filePath); } - function nockIsGCE() { + function nockIsGCE(opts = {universeDomain: 'my-universe.com'}) { const primary = nock(host).get(instancePath).reply(200, {}, HEADERS); const secondary = nock(SECONDARY_HOST_ADDRESS) .get(instancePath) .reply(200, {}, HEADERS); + const universeDomain = nock(HOST_ADDRESS) + .get(universeDomainPath) + .reply(200, opts.universeDomain, HEADERS); return { done: () => { - try { - primary.done(); - secondary.done(); - } catch (_err) { - // secondary can sometimes complete prior to primary. - } + return Promise.allSettled([ + primary.done(), + secondary.done(), + universeDomain.done(), + ]); }, }; } @@ -1085,11 +1088,10 @@ describe('googleauth', () => { // * Well-known file is not set. // * Running on GCE is set to true. mockWindows(); - sandbox.stub(auth, '_checkIsGCE').rejects('🤮'); - await assert.rejects( - auth.getApplicationDefault(), - /Unexpected error determining execution environment/ - ); + const e = new Error('abc'); + + sandbox.stub(auth, '_checkIsGCE').rejects(e); + await assert.rejects(auth.getApplicationDefault(), e); }); it('getApplicationDefault should also get project ID', async () => { @@ -1128,25 +1130,19 @@ describe('googleauth', () => { }); it('getCredentials should get metadata from the server when running on GCE', async () => { - const response = { - default: { - email: 'test-creds@test-creds.iam.gserviceaccount.com', - private_key: null, - }, - }; + const clientEmail = 'test-creds@test-creds.iam.gserviceaccount.com'; + const universeDomain = 'my-amazing-universe.com'; const scopes = [ - nockIsGCE(), + nockIsGCE({universeDomain}), createGetProjectIdNock(), - nock(host).get(svcAccountPath).reply(200, response, HEADERS), + nock(host).get(svcAccountPath).reply(200, clientEmail, HEADERS), ]; await auth._checkIsGCE(); assert.strictEqual(true, auth.isGCE); const body = await auth.getCredentials(); assert.ok(body); - assert.strictEqual( - body.client_email, - 'test-creds@test-creds.iam.gserviceaccount.com' - ); + assert.strictEqual(body.client_email, clientEmail); + assert.strictEqual(body.universe_domain, universeDomain); assert.strictEqual(body.private_key, undefined); scopes.forEach(s => s.done()); }); @@ -1415,9 +1411,7 @@ describe('googleauth', () => { const data = 'abc123'; scopes.push( nock(iamUri).post(iamPath).reply(200, {signedBlob}), - nock(host) - .get(svcAccountPath) - .reply(200, {default: {email, private_key: privateKey}}, HEADERS) + nock(host).get(svcAccountPath).reply(200, email, HEADERS) ); const value = await auth.sign(data); scopes.forEach(x => x.done()); @@ -1556,6 +1550,23 @@ describe('googleauth', () => { assert.fail('failed to throw'); }); + describe('getUniverseDomain', () => { + it('should prefer `clientOptions` > metadata service when available', async () => { + const universeDomain = 'my.universe.com'; + const auth = new GoogleAuth({clientOptions: {universeDomain}}); + + assert.equal(await auth.getUniverseDomain(), universeDomain); + }); + + it('should use the metadata service if on GCP', async () => { + const universeDomain = 'my.universe.com'; + const scope = nockIsGCE({universeDomain}); + + assert.equal(await auth.getUniverseDomain(), universeDomain); + await scope.done(); + }); + }); + function mockApplicationDefaultCredentials(path: string) { // Fake a home directory in our fixtures path. mockEnvVar('GCLOUD_PROJECT', 'my-fake-project'); From cb78a1bfb4104e05be85877fee56d703f749e525 Mon Sep 17 00:00:00 2001 From: salrashid123 Date: Wed, 29 Nov 2023 13:11:34 -0500 Subject: [PATCH 479/662] feat: Add impersonated signer (#1694) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adding fixes * fix sample * return signedblobresponse except for gcs clients Signed-off-by: salrashid123 * update test * fixes * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Signed-off-by: salrashid123 Co-authored-by: Daniel Bankhead Co-authored-by: Owl Bot --- README.md | 1 + samples/README.md | 18 ++++++++ samples/signBlobImpersonated.js | 73 +++++++++++++++++++++++++++++++++ src/auth/googleauth.ts | 10 +++++ src/auth/impersonated.ts | 29 +++++++++++++ test/test.googleauth.ts | 56 +++++++++++++++++++++++++ test/test.impersonated.ts | 44 ++++++++++++++++++++ 7 files changed, 231 insertions(+) create mode 100644 samples/signBlobImpersonated.js diff --git a/README.md b/README.md index ba9a6417..884cbbc9 100644 --- a/README.md +++ b/README.md @@ -1217,6 +1217,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar | Oauth2-code Verifier | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2-codeVerifier.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2-codeVerifier.js,samples/README.md) | | Oauth2 | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2.js,samples/README.md) | | Sign Blob | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlob.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlob.js,samples/README.md) | +| Sign Blob Impersonated | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlobImpersonated.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlobImpersonated.js,samples/README.md) | | Verify Google Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyGoogleIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyGoogleIdToken.js,samples/README.md) | | Verifying ID Tokens from Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken-iap.js,samples/README.md) | | Verify Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken.js,samples/README.md) | diff --git a/samples/README.md b/samples/README.md index cf16c381..115dba68 100644 --- a/samples/README.md +++ b/samples/README.md @@ -30,6 +30,7 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra * [Oauth2-code Verifier](#oauth2-code-verifier) * [Oauth2](#oauth2) * [Sign Blob](#sign-blob) + * [Sign Blob Impersonated](#sign-blob-impersonated) * [Verify Google Id Token](#verify-google-id-token) * [Verifying ID Tokens from Identity-Aware Proxy (IAP)](#verifying-id-tokens-from-identity-aware-proxy-iap) * [Verify Id Token](#verify-id-token) @@ -359,6 +360,23 @@ __Usage:__ +### Sign Blob Impersonated + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlobImpersonated.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlobImpersonated.js,samples/README.md) + +__Usage:__ + + +`node samples/signBlobImpersonated.js` + + +----- + + + + ### Verify Google Id Token View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyGoogleIdToken.js). diff --git a/samples/signBlobImpersonated.js b/samples/signBlobImpersonated.js new file mode 100644 index 00000000..4cb95a62 --- /dev/null +++ b/samples/signBlobImpersonated.js @@ -0,0 +1,73 @@ +// Copyright 2023 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. + +'use strict'; + +const {GoogleAuth, Impersonated} = require('google-auth-library'); + +/** + * Use the iamcredentials API to sign a blob of data. + */ +async function main() { + // get source credentials + const auth = new GoogleAuth(); + const client = await auth.getClient(); + + // First impersonate + const scopes = ['https://www.googleapis.com/auth/cloud-platform']; + + const targetPrincipal = 'target@project.iam.gserviceaccount.com'; + const targetClient = new Impersonated({ + sourceClient: client, + targetPrincipal: targetPrincipal, + lifetime: 30, + delegates: [], + targetScopes: [scopes], + }); + + const signedData = await targetClient.sign('some data'); + console.log(signedData.signedBlob); + + // or use the client to create a GCS signedURL + // const { Storage } = require('@google-cloud/storage'); + + // const projectId = 'yourProjectID' + // const bucketName = 'yourBucket' + // const objectName = 'yourObject' + + // // use the impersonated client to access gcs + // const storageOptions = { + // projectId, + // authClient: targetClient, + // }; + + // const storage = new Storage(storageOptions); + + // const signOptions = { + // version: 'v4', + // action: 'read', + // expires: Date.now() + 15 * 60 * 1000, // 15 minutes + // }; + + // const signedURL = await storage + // .bucket(bucketName) + // .file(objectName) + // .getSignedUrl(signOptions); + + // console.log(signedURL); +} + +main().catch(e => { + console.error(e); + throw e; +}); diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 13ab18a4..e20ecf47 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -933,6 +933,10 @@ export class GoogleAuth { private async getCredentialsAsync(): Promise { const client = await this.getClient(); + if (client instanceof Impersonated) { + return {client_email: client.getTargetPrincipal()}; + } + if (client instanceof BaseExternalAccountClient) { const serviceAccountEmail = client.getServiceAccountEmail(); if (serviceAccountEmail) { @@ -1059,6 +1063,12 @@ export class GoogleAuth { */ async sign(data: string): Promise { const client = await this.getClient(); + + if (client instanceof Impersonated) { + const signed = await client.sign(data); + return signed.signedBlob; + } + const crypto = createCrypto(); if (client instanceof JWT && client.key) { const sign = await crypto.sign(client.key, data); diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index 24debd81..ac24fd7a 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -22,6 +22,7 @@ import { import {AuthClient} from './authclient'; import {IdTokenProvider} from './idtokenclient'; import {GaxiosError} from 'gaxios'; +import {SignBlobResponse} from './googleauth'; export interface ImpersonatedOptions extends OAuth2ClientOptions { /** @@ -126,6 +127,34 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { this.endpoint = options.endpoint ?? 'https://iamcredentials.googleapis.com'; } + /** + * Signs some bytes. + * + * {@link https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob Reference Documentation} + * @param blobToSign String to sign. + * @return denoting the keyyID and signedBlob in base64 string + */ + async sign(blobToSign: string): Promise { + await this.sourceClient.getAccessToken(); + const name = `projects/-/serviceAccounts/${this.targetPrincipal}`; + const u = `${this.endpoint}/v1/${name}:signBlob`; + const body = { + delegates: this.delegates, + payload: Buffer.from(blobToSign).toString('base64'), + }; + const res = await this.sourceClient.request({ + url: u, + data: body, + method: 'POST', + }); + return res.data; + } + + /** The service account email to be impersonated. */ + getTargetPrincipal(): string { + return this.targetPrincipal; + } + /** * Refreshes the access token. * @param refreshToken Unused parameter diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 6ba7f4e9..137e40cd 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1578,6 +1578,62 @@ describe('googleauth', () => { .post('/token') .reply(200, {}); } + describe('for impersonated types', () => { + describe('for impersonated credentials signing', () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + 3600 * 1000).toISOString(), + }; + + it('should use IAMCredentials signBlob endpoint when impersonation is used', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/impersonated_application_default_credentials.json' + ); + + // Set up a mock to explicity return the Project ID, as needed for impersonated ADC + mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); + + const auth = new GoogleAuth(); + const client = await auth.getClient(); + + const email = 'target@project.iam.gserviceaccount.com'; + const iamUri = 'https://iamcredentials.googleapis.com'; + const iamPath = `/v1/projects/-/serviceAccounts/${email}:signBlob`; + const signedBlob = 'erutangis'; + const keyId = '12345'; + const data = 'abc123'; + const scopes = [ + nock('https://oauth2.googleapis.com').post('/token').reply(200, { + access_token: saSuccessResponse.accessToken, + }), + nock(iamUri) + .post( + iamPath, + { + delegates: [], + payload: Buffer.from(data, 'utf-8').toString('base64'), + }, + { + reqheaders: { + Authorization: `Bearer ${saSuccessResponse.accessToken}`, + 'Content-Type': 'application/json', + }, + } + ) + .reply(200, {keyId: keyId, signedBlob: signedBlob}), + ]; + + const signed = await auth.sign(data); + + scopes.forEach(x => x.done()); + assert(client instanceof Impersonated); + assert.strictEqual(signed, signedBlob); + }); + }); + }); describe('for external_account types', () => { let fromJsonSpy: sinon.SinonSpy< diff --git a/test/test.impersonated.ts b/test/test.impersonated.ts index 8b4886e0..d8bc2b06 100644 --- a/test/test.impersonated.ts +++ b/test/test.impersonated.ts @@ -455,4 +455,48 @@ describe('impersonated', () => { scopes.forEach(s => s.done()); }); + + it('should sign a blob', async () => { + const expectedKeyID = '12345'; + const expectedSignedBlob = 'signed'; + const expectedBlobToSign = 'signme'; + const expectedDeligates = ['deligate-1', 'deligate-2']; + const email = 'target@project.iam.gserviceaccount.com'; + + const scopes = [ + createGTokenMock({ + access_token: 'abc123', + }), + nock('https://iamcredentials.googleapis.com') + .post( + `/v1/projects/-/serviceAccounts/${email}:signBlob`, + (body: {delegates: string[]; payload: string}) => { + assert.strictEqual( + body.payload, + Buffer.from(expectedBlobToSign).toString('base64') + ); + assert.deepStrictEqual(body.delegates, expectedDeligates); + return true; + } + ) + .reply(200, { + keyId: expectedKeyID, + signedBlob: expectedSignedBlob, + }), + ]; + + const impersonated = new Impersonated({ + sourceClient: createSampleJWTClient(), + targetPrincipal: email, + lifetime: 30, + delegates: expectedDeligates, + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + + const resp = await impersonated.sign(expectedBlobToSign); + assert.equal(email, impersonated.getTargetPrincipal()); + assert.equal(resp.keyId, expectedKeyID); + assert.equal(resp.signedBlob, expectedSignedBlob); + scopes.forEach(s => s.done()); + }); }); From c5080a0d56b52b90b5d26cfc5f7343afb58199a4 Mon Sep 17 00:00:00 2001 From: Javien Lee Date: Thu, 30 Nov 2023 02:40:41 +0800 Subject: [PATCH 480/662] fix: verifier algorithm (#1696) Co-authored-by: Daniel Bankhead --- src/crypto/node/crypto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/node/crypto.ts b/src/crypto/node/crypto.ts index eaaff400..7d045f2b 100644 --- a/src/crypto/node/crypto.ts +++ b/src/crypto/node/crypto.ts @@ -29,7 +29,7 @@ export class NodeCrypto implements Crypto { data: string | Buffer, signature: string ): Promise { - const verifier = crypto.createVerify('sha256'); + const verifier = crypto.createVerify('RSA-SHA256'); verifier.update(data); verifier.end(); return verifier.verify(pubkey, signature, 'base64'); From 0c361a13f50c79804a80b0152a70b3a2bd868d3b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:00:32 -0800 Subject: [PATCH 481/662] chore(main): release 9.3.0 (#1698) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 516bd87c..0fef1b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.3.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.2.0...v9.3.0) (2023-11-29) + + +### Features + +* Add impersonated signer ([#1694](https://github.com/googleapis/google-auth-library-nodejs/issues/1694)) ([cb78a1b](https://github.com/googleapis/google-auth-library-nodejs/commit/cb78a1bfb4104e05be85877fee56d703f749e525)) +* Retrieve `universe_domain` for Compute clients ([#1692](https://github.com/googleapis/google-auth-library-nodejs/issues/1692)) ([a735ec5](https://github.com/googleapis/google-auth-library-nodejs/commit/a735ec5f5e280a1c47f0f4d5e99acf228d5a320a)) + + +### Bug Fixes + +* Verifier algorithm ([#1696](https://github.com/googleapis/google-auth-library-nodejs/issues/1696)) ([c5080a0](https://github.com/googleapis/google-auth-library-nodejs/commit/c5080a0d56b52b90b5d26cfc5f7343afb58199a4)) + ## [9.2.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.1.0...v9.2.0) (2023-10-26) diff --git a/package.json b/package.json index 97c701ff..97ba824b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.2.0", + "version": "9.3.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index a313b6b4..d16bc959 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^12.0.0", - "google-auth-library": "^9.2.0", + "google-auth-library": "^9.3.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 3e2cb98bea5abe416dd76274aadfd08d9497c2f0 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 29 Nov 2023 14:04:07 -0800 Subject: [PATCH 482/662] test: fix flaky tests when ran on GCP (#1701) --- test/test.googleauth.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 137e40cd..c22057ef 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -212,9 +212,9 @@ describe('googleauth', () => { return { done: () => { return Promise.allSettled([ - primary.done(), - secondary.done(), - universeDomain.done(), + (async () => primary.done())(), + (async () => secondary.done())(), + (async () => universeDomain.done())(), ]); }, }; From 615bdd92d1eb1a2a5b8d8c2675fd53350d5fbc21 Mon Sep 17 00:00:00 2001 From: Joram Ruitenschild Date: Thu, 30 Nov 2023 15:54:11 +0100 Subject: [PATCH 483/662] feat: Update gcp metadata dependency (#1703) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97ba824b..6e4e59cd 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.0.0", - "gcp-metadata": "^6.0.0", + "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" }, From e8966d19599f751b7b14364c67ce601b6e4793a0 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 07:22:01 -0800 Subject: [PATCH 484/662] chore(main): release 9.4.0 (#1704) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fef1b60..d924a8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.4.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.3.0...v9.4.0) (2023-11-30) + + +### Features + +* Update gcp metadata dependency ([#1703](https://github.com/googleapis/google-auth-library-nodejs/issues/1703)) ([615bdd9](https://github.com/googleapis/google-auth-library-nodejs/commit/615bdd92d1eb1a2a5b8d8c2675fd53350d5fbc21)) + ## [9.3.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.2.0...v9.3.0) (2023-11-29) diff --git a/package.json b/package.json index 6e4e59cd..eadee9c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.3.0", + "version": "9.4.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index d16bc959..75773065 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^12.0.0", - "google-auth-library": "^9.3.0", + "google-auth-library": "^9.4.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 704674fe14750d31e7d3a58d6f9b2387c89fb184 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 30 Nov 2023 23:18:00 -0800 Subject: [PATCH 485/662] fix: Support 404 When `GaxiosError` != `GaxiosError` (#1707) * fix: Support 404 When GaxiosError != GaxiosError * fix: optionalize * refactor: Even more robust for `GaxiosError` != `GaxiosError` Older versions of gaxios may not have `.status`, but only `.response.status` --- package.json | 2 +- src/auth/googleauth.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index eadee9c0..7e4036df 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.0.0", + "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index e20ecf47..b5d66c70 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -311,7 +311,7 @@ export class GoogleAuth { universeDomain = await gcpMetadata.universe('universe_domain'); universeDomain ||= DEFAULT_UNIVERSE; } catch (e) { - if (e instanceof GaxiosError && e.status === 404) { + if (e && (e as GaxiosError)?.response?.status === 404) { universeDomain = DEFAULT_UNIVERSE; } else { throw e; From b4954c296a0956f84f25f61d701ad385e5476a48 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 23:24:34 -0800 Subject: [PATCH 486/662] chore(main): release 9.4.1 (#1708) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d924a8b9..d2c9bfdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.4.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.4.0...v9.4.1) (2023-12-01) + + +### Bug Fixes + +* Support 404 When `GaxiosError` != `GaxiosError` ([#1707](https://github.com/googleapis/google-auth-library-nodejs/issues/1707)) ([704674f](https://github.com/googleapis/google-auth-library-nodejs/commit/704674fe14750d31e7d3a58d6f9b2387c89fb184)) + ## [9.4.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.3.0...v9.4.0) (2023-11-30) diff --git a/package.json b/package.json index 7e4036df..ba3b1c65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.4.0", + "version": "9.4.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 75773065..6f288923 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^12.0.0", - "google-auth-library": "^9.4.0", + "google-auth-library": "^9.4.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 7ae4aaea0311de1a3e10f4e86a1ef93ff00656b5 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 14 Dec 2023 20:27:47 +0100 Subject: [PATCH 487/662] fix(deps): update dependency @googleapis/iam to v13 (#1710) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 6f288923..aa580414 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^12.0.0", + "@googleapis/iam": "^13.0.0", "google-auth-library": "^9.4.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 72bfad3c539e55c3c8a985f3ac3ab9222ad7dc92 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 3 Jan 2024 14:56:10 -0800 Subject: [PATCH 488/662] chore: Skip IAP (#1718) --- samples/test/jwt.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/test/jwt.test.js b/samples/test/jwt.test.js index 5ad080ec..096bd325 100644 --- a/samples/test/jwt.test.js +++ b/samples/test/jwt.test.js @@ -67,7 +67,7 @@ describe('samples', () => { assert.match(output, /What's next?/); }); - it('should fetch ID token for IAP', async () => { + it.skip('should fetch ID token for IAP', async () => { // process.env.IAP_URL should be an App Engine app, protected with IAP: const url = process.env.IAP_URL || 'https://nodejs-docs-samples-iap.appspot.com'; From 88ba8ba34361fbd069571fbda54bbe2fb519ec8e Mon Sep 17 00:00:00 2001 From: piaxc Date: Wed, 3 Jan 2024 16:27:03 -0800 Subject: [PATCH 489/662] Docs: update links into authentication content on cloud.google.com (#1709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update links into c.g.c authentication content Also remove references to service account keys as per go/mitigating-cloud-auth-risk. * Update idtokenclient.ts The compute docs don't really talk about ID tokens, so linking to Cloud authentication docs instead. * Update computeclient.ts The referenced Compute page didn't talk about getting an access token from the metadata server anymore, updated link. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Daniel Bankhead Co-authored-by: Owl Bot --- src/auth/computeclient.ts | 2 +- src/auth/idtokenclient.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index ccecb66a..35626b0a 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -44,7 +44,7 @@ export class Compute extends OAuth2Client { * Google Compute Engine service account credentials. * * Retrieve access token from the metadata server. - * See: https://developers.google.com/compute/docs/authentication + * See: https://cloud.google.com/compute/docs/access/authenticate-workloads#applications */ constructor(options: ComputeOptions = {}) { super(options); diff --git a/src/auth/idtokenclient.ts b/src/auth/idtokenclient.ts index 03e186ac..5bd62b52 100644 --- a/src/auth/idtokenclient.ts +++ b/src/auth/idtokenclient.ts @@ -42,8 +42,8 @@ export class IdTokenClient extends OAuth2Client { /** * Google ID Token client * - * Retrieve access token from the metadata server. - * See: https://developers.google.com/compute/docs/authentication + * Retrieve ID token from the metadata server. + * See: https://cloud.google.com/docs/authentication/get-id-token#metadata-server */ constructor(options: IdTokenOptions) { super(options); From 846c8e96cc36feb05148c16e12127e805e388189 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 4 Jan 2024 12:07:12 -0800 Subject: [PATCH 490/662] fix: expand credentials type (#1719) --- src/auth/googleauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index b5d66c70..ed51e3dc 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -102,7 +102,7 @@ export interface GoogleAuthOptions { * Object containing client_email and private_key properties, or the * external account client options. */ - credentials?: CredentialBody | ExternalAccountClientOptions; + credentials?: JWTInput | ExternalAccountClientOptions; /** * Options object passed to the constructor of the client From 23177143dfd46bb369b9c930cb84129854a7f94b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 10 Jan 2024 11:39:46 -0800 Subject: [PATCH 491/662] chore(main): release 9.4.2 (#1712) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c9bfdf..3e561467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.4.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.4.1...v9.4.2) (2024-01-04) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v13 ([#1710](https://github.com/googleapis/google-auth-library-nodejs/issues/1710)) ([7ae4aae](https://github.com/googleapis/google-auth-library-nodejs/commit/7ae4aaea0311de1a3e10f4e86a1ef93ff00656b5)) +* Expand credentials type ([#1719](https://github.com/googleapis/google-auth-library-nodejs/issues/1719)) ([846c8e9](https://github.com/googleapis/google-auth-library-nodejs/commit/846c8e96cc36feb05148c16e12127e805e388189)) + ## [9.4.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.4.0...v9.4.1) (2023-12-01) diff --git a/package.json b/package.json index ba3b1c65..96c153e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.4.1", + "version": "9.4.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index aa580414..c5b3dec3 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^13.0.0", - "google-auth-library": "^9.4.1", + "google-auth-library": "^9.4.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 594bf2cc808c03733274d6b08d92f1d4b12dd630 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 22 Jan 2024 20:48:43 +0100 Subject: [PATCH 492/662] fix(deps): update dependency @googleapis/iam to v14 (#1725) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index c5b3dec3..88655401 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^13.0.0", + "@googleapis/iam": "^14.0.0", "google-auth-library": "^9.4.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 058a5035e3e4df35663c6b3adef2dda617271849 Mon Sep 17 00:00:00 2001 From: Alejandro Marco <78371908+amalej@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:32:50 +0800 Subject: [PATCH 493/662] fix: typos in samples (#1728) * fix: typos in samples * chore: Fix copyright * chore: Fix copyright --------- Co-authored-by: Daniel Bankhead Co-authored-by: Daniel Bankhead --- samples/jwt.js | 7 ++++--- samples/keyfile.js | 7 ++++--- samples/scripts/externalclient-setup.js | 2 +- samples/test/externalclient.test.js | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/samples/jwt.js b/samples/jwt.js index 3110c8f9..62b9cccd 100644 --- a/samples/jwt.js +++ b/samples/jwt.js @@ -1,9 +1,10 @@ -// Copyright 2017, Google, Inc. +// Copyright 2017 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 +// 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, @@ -27,7 +28,7 @@ const {JWT} = require('google-auth-library'); const fs = require('fs'); async function main( - // Full path to the sevice account credential + // Full path to the service account credential keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS ) { const keys = JSON.parse(fs.readFileSync(keyFile, 'utf8')); diff --git a/samples/keyfile.js b/samples/keyfile.js index 5915f518..3018d843 100644 --- a/samples/keyfile.js +++ b/samples/keyfile.js @@ -1,9 +1,10 @@ -// Copyright 2018, Google, LLC. +// Copyright 2018 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 +// 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, @@ -22,7 +23,7 @@ const {GoogleAuth} = require('google-auth-library'); * Acquire a client, and make a request to an API that's enabled by default. */ async function main( - // Full path to the sevice account credential + // Full path to the service account credential keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS ) { const auth = new GoogleAuth({ diff --git a/samples/scripts/externalclient-setup.js b/samples/scripts/externalclient-setup.js index 21e75954..caf24bd7 100755 --- a/samples/scripts/externalclient-setup.js +++ b/samples/scripts/externalclient-setup.js @@ -165,7 +165,7 @@ async function main(config) { const awsAudience = `//iam.googleapis.com/${poolResourcePath}/providers/${awsProviderId}`; // Allow service account impersonation. - // Get the existing IAM policity bindings on the current service account. + // Get the existing IAM policy bindings on the current service account. response = await iam.projects.serviceAccounts.getIamPolicy({ resource: `projects/${projectId}/serviceAccounts/${clientEmail}`, }); diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js index 6efdc3f3..b3e231c3 100644 --- a/samples/test/externalclient.test.js +++ b/samples/test/externalclient.test.js @@ -50,7 +50,7 @@ // AWS provider tests for AWS credentials // ------------------------------------- // The test suite will also run tests for AWS credentials. This works as -// follows. (Note prequisite setup is needed. This is documented in +// follows. (Note prerequisite setup is needed. This is documented in // externalclient-setup.js). // - iamcredentials:generateIdToken is used to generate a Google ID token using // the service account access token. The service account client_id is used as From eec82f5f48a250744b5c3200ef247c3eae184e2f Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 25 Jan 2024 12:18:26 -0800 Subject: [PATCH 494/662] feat: Improve Universe Domain Ergonomics (#1732) --- src/auth/googleauth.ts | 17 +++++++++++++++-- src/index.ts | 2 +- test/test.googleauth.ts | 7 +++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index ed51e3dc..6b3d6daa 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -122,6 +122,14 @@ export interface GoogleAuthOptions { * Your project ID. */ projectId?: string; + + /** + * The default service domain for a given Cloud universe. + * + * This is an ergonomic equivalent to {@link clientOptions}'s `universeDomain` + * property and will be set for all generated {@link AuthClient}s. + */ + universeDomain?: string; } export const CLOUD_SDK_CLIENT_ID = @@ -175,7 +183,7 @@ export class GoogleAuth { defaultScopes?: string | string[]; private keyFilename?: string; private scopes?: string | string[]; - private clientOptions?: AuthClientOptions; + private clientOptions: AuthClientOptions = {}; /** * The cached universe domain. @@ -208,7 +216,12 @@ export class GoogleAuth { this.keyFilename = opts.keyFilename || opts.keyFile; this.scopes = opts.scopes; this.jsonContent = opts.credentials || null; - this.clientOptions = opts.clientOptions; + this.clientOptions = opts.clientOptions || {}; + + if (opts.universeDomain) { + this.clientOptions.universeDomain = opts.universeDomain; + this.#universeDomain = opts.universeDomain; + } } // GAPIC client libraries should always use self-signed JWTs. The following diff --git a/src/index.ts b/src/index.ts index ea6c2764..5225dd53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ import {GoogleAuth} from './auth/googleauth'; export * as gcpMetadata from 'gcp-metadata'; -export {AuthClient} from './auth/authclient'; +export {AuthClient, DEFAULT_UNIVERSE} from './auth/authclient'; export {Compute, ComputeOptions} from './auth/computeclient'; export { CredentialBody, diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index c22057ef..f7441567 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1551,6 +1551,13 @@ describe('googleauth', () => { }); describe('getUniverseDomain', () => { + it('should prefer `universeDomain` > metadata service when available', async () => { + const universeDomain = 'my.universe.com'; + const auth = new GoogleAuth({universeDomain}); + + assert.equal(await auth.getUniverseDomain(), universeDomain); + }); + it('should prefer `clientOptions` > metadata service when available', async () => { const universeDomain = 'my.universe.com'; const auth = new GoogleAuth({clientOptions: {universeDomain}}); From bcefe96b3d4a3af520e7070c97c15acc63905f69 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:23:55 -0800 Subject: [PATCH 495/662] chore(main): release 9.5.0 (#1730) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e561467..d2f675ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.5.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.4.2...v9.5.0) (2024-01-25) + + +### Features + +* Improve Universe Domain Ergonomics ([#1732](https://github.com/googleapis/google-auth-library-nodejs/issues/1732)) ([eec82f5](https://github.com/googleapis/google-auth-library-nodejs/commit/eec82f5f48a250744b5c3200ef247c3eae184e2f)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v14 ([#1725](https://github.com/googleapis/google-auth-library-nodejs/issues/1725)) ([594bf2c](https://github.com/googleapis/google-auth-library-nodejs/commit/594bf2cc808c03733274d6b08d92f1d4b12dd630)) +* Typos in samples ([#1728](https://github.com/googleapis/google-auth-library-nodejs/issues/1728)) ([058a503](https://github.com/googleapis/google-auth-library-nodejs/commit/058a5035e3e4df35663c6b3adef2dda617271849)) + ## [9.4.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.4.1...v9.4.2) (2024-01-04) diff --git a/package.json b/package.json index 96c153e1..663c658a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.4.2", + "version": "9.5.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 88655401..ab509181 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^14.0.0", - "google-auth-library": "^9.4.2", + "google-auth-library": "^9.5.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 7e9876e2496b073220ca270368da7e9522da88f9 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 26 Jan 2024 10:59:32 -0800 Subject: [PATCH 496/662] feat: Use self-signed JWTs when non-default Universe Domains (#1722) * feat: Use self-signed JWTs when non-default Universe Domains * feat: AL-9 * fix: remove unused --- src/auth/jwtclient.ts | 11 +++++++- test/test.jwt.ts | 66 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index d25f4146..9e12b23c 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -24,6 +24,7 @@ import { OAuth2ClientOptions, RequestMetadataResponse, } from './oauth2client'; +import {DEFAULT_UNIVERSE} from './authclient'; export interface JWTOptions extends OAuth2ClientOptions { email?: string; @@ -119,7 +120,15 @@ export class JWT extends OAuth2Client implements IdTokenProvider { url = this.defaultServicePath ? `https://${this.defaultServicePath}/` : url; const useSelfSignedJWT = (!this.hasUserScopes() && url) || - (this.useJWTAccessWithScope && this.hasAnyScopes()); + (this.useJWTAccessWithScope && this.hasAnyScopes()) || + this.universeDomain !== DEFAULT_UNIVERSE; + + if (this.subject && this.universeDomain !== DEFAULT_UNIVERSE) { + throw new RangeError( + `Service Account user is configured for the credential. Domain-wide delegation is not supported in universes other than ${DEFAULT_UNIVERSE}` + ); + } + if (!this.apiKey && useSelfSignedJWT) { if ( this.additionalClaims && diff --git a/test/test.jwt.ts b/test/test.jwt.ts index f20fcbe5..fc11bd02 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -1007,6 +1007,72 @@ describe('jwt', () => { ); }); + it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, universeDomain = not default universe', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + universeDomain: 'my-universe.com', + }); + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + undefined + ); + }); + + it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true, universeDomain = not default universe', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + universeDomain: 'my-universe.com', + }); + jwt.useJWTAccessWithScope = true; + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + ['scope1', 'scope2'] + ); + }); + + it('throws on domain-wide delegation on non-default universe', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + universeDomain: 'my-universe.com', + }); + jwt.useJWTAccessWithScope = true; + jwt.defaultScopes = ['scope1', 'scope2']; + + await assert.rejects( + () => jwt.getRequestHeaders('https//beepboop.googleapis.com'), + /Domain-wide delegation is not supported in universes other than/ + ); + }); + it('does not use self signed JWT if target_audience provided', async () => { const JWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: sinon.stub().returns({}), From effbf87f6f0fd11a0cb1c749dad81737926dc436 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 26 Jan 2024 11:39:39 -0800 Subject: [PATCH 497/662] feat: Open More Endpoints for Customization (#1721) * feat: Allow custom STS Access Token URL for Downscoped Clients * feat: Open More Endpoints for Customization * docs: Update * docs: Update * docs: Update * chore: remove unrelated * refactor: base endpoints on universe domain --- src/auth/baseexternalclient.ts | 48 +++++---- src/auth/downscopedclient.ts | 20 +++- src/auth/googleauth.ts | 25 +++-- src/auth/oauth2client.ts | 173 +++++++++++++++++++++----------- src/auth/stscredentials.ts | 4 +- test/externalclienthelper.ts | 5 +- test/test.baseexternalclient.ts | 37 ------- test/test.downscopedclient.ts | 36 +++++++ test/test.externalclient.ts | 38 ------- test/test.googleauth.ts | 4 +- test/test.identitypoolclient.ts | 38 ------- 11 files changed, 222 insertions(+), 206 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 6496b143..89bb610a 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -53,12 +53,13 @@ export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000; * 3. external_Account => non-GCP service (eg. AWS, Azure, K8s) */ export const EXTERNAL_ACCOUNT_TYPE = 'external_account'; -/** Cloud resource manager URL used to retrieve project information. */ +/** + * Cloud resource manager URL used to retrieve project information. + * + * @deprecated use {@link BaseExternalAccountClient.cloudResourceManagerURL} instead + **/ export const CLOUD_RESOURCE_MANAGER = 'https://cloudresourcemanager.googleapis.com/v1/projects/'; -/** The workforce audience pattern. */ -const WORKFORCE_AUDIENCE_PATTERN = - '//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); @@ -88,6 +89,12 @@ export interface BaseExternalAccountClientOptions client_id?: string; client_secret?: string; workforce_pool_user_project?: string; + scopes?: string[]; + /** + * @example + * https://cloudresourcemanager.googleapis.com/v1/projects/ + **/ + cloud_resource_manager_url?: string | URL; } /** @@ -150,6 +157,13 @@ export abstract class BaseExternalAccountClient extends AuthClient { public projectNumber: string | null; private readonly configLifetimeRequested: boolean; protected credentialSourceType?: string; + /** + * @example + * ```ts + * new URL('https://cloudresourcemanager.googleapis.com/v1/projects/'); + * ``` + */ + protected cloudResourceManagerURL: URL | string; /** * Instantiate a BaseExternalAccountClient instance using the provided JSON * object loaded from an external account credentials file. @@ -195,6 +209,11 @@ export abstract class BaseExternalAccountClient extends AuthClient { serviceAccountImpersonation ).get('token_lifetime_seconds'); + this.cloudResourceManagerURL = new URL( + opts.get('cloud_resource_manager_url') || + `https://cloudresourcemanager.${this.universeDomain}/v1/projects/` + ); + if (clientId) { this.clientAuth = { confidentialClientType: 'basic', @@ -204,22 +223,11 @@ export abstract class BaseExternalAccountClient extends AuthClient { } this.stsCredential = new sts.StsCredentials(tokenUrl, this.clientAuth); - // Default OAuth scope. This could be overridden via public property. - this.scopes = [DEFAULT_OAUTH_SCOPE]; + this.scopes = opts.get('scopes') || [DEFAULT_OAUTH_SCOPE]; this.cachedAccessToken = null; this.audience = opts.get('audience'); this.subjectTokenType = subjectTokenType; this.workforcePoolUserProject = workforcePoolUserProject; - const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN); - if ( - this.workforcePoolUserProject && - !this.audience.match(workforceAudiencePattern) - ) { - throw new Error( - 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' - ); - } this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl; this.serviceAccountImpersonationLifetime = serviceAccountImpersonationLifetime; @@ -360,7 +368,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { const headers = await this.getRequestHeaders(); const response = await this.transporter.request({ headers, - url: `${CLOUD_RESOURCE_MANAGER}${projectNumber}`, + url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`, responseType: 'json', }); this.projectId = response.data.projectId; @@ -576,11 +584,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { // be normalized. if (typeof this.scopes === 'string') { return [this.scopes]; - } else if (typeof this.scopes === 'undefined') { - return [DEFAULT_OAUTH_SCOPE]; - } else { - return this.scopes; } + + return this.scopes || [DEFAULT_OAUTH_SCOPE]; } private getMetricsHeaderValue(): string { diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 92238b40..b4a7a019 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -39,8 +39,6 @@ const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; * The requested token exchange subject_token_type: rfc8693#section-2.1 */ const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; -/** The STS access token exchange end point. */ -const STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1/token'; /** * The maximum number of access boundary rules a Credential Access Boundary @@ -75,6 +73,13 @@ export interface CredentialAccessBoundary { accessBoundary: { accessBoundaryRules: AccessBoundaryRule[]; }; + /** + * An optional STS access token exchange endpoint. + * + * @example + * 'https://sts.googleapis.com/v1/token' + */ + tokenURL?: string | URL; } /** Defines an upper bound of permissions on a particular resource. */ @@ -135,6 +140,12 @@ export class DownscopedClient extends AuthClient { quotaProjectId?: string ) { super({...additionalOptions, quotaProjectId}); + + // extract and remove `tokenURL` as it is not officially a part of the credentialAccessBoundary + this.credentialAccessBoundary = {...credentialAccessBoundary}; + const tokenURL = this.credentialAccessBoundary.tokenURL; + delete this.credentialAccessBoundary.tokenURL; + // Check 1-10 Access Boundary Rules are defined within Credential Access // Boundary. if ( @@ -162,7 +173,10 @@ export class DownscopedClient extends AuthClient { } } - this.stsCredential = new sts.StsCredentials(STS_ACCESS_TOKEN_URL); + this.stsCredential = new sts.StsCredentials( + tokenURL || `https://sts.${this.universeDomain}/v1/token` + ); + this.cachedDownscopedAccessToken = null; } diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 6b3d6daa..b4f6cf15 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -1073,9 +1073,20 @@ export class GoogleAuth { * 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. + * @param endpoint A custom endpoint to use. + * + * @example + * ``` + * sign('data', 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/'); + * ``` */ - async sign(data: string): Promise { + async sign(data: string, endpoint?: string): Promise { const client = await this.getClient(); + const universe = await this.getUniverseDomain(); + + endpoint = + endpoint || + `https://iamcredentials.${universe}/v1/projects/-/serviceAccounts/`; if (client instanceof Impersonated) { const signed = await client.sign(data); @@ -1093,24 +1104,24 @@ export class GoogleAuth { throw new Error('Cannot sign data without `client_email`.'); } - return this.signBlob(crypto, creds.client_email, data); + return this.signBlob(crypto, creds.client_email, data, endpoint); } private async signBlob( crypto: Crypto, emailOrUniqueId: string, - data: string + data: string, + endpoint: string ): Promise { - const url = - 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' + - `${emailOrUniqueId}:signBlob`; + const url = new URL(endpoint + `${emailOrUniqueId}:signBlob`); const res = await this.request({ method: 'POST', - url, + url: url.href, data: { payload: crypto.encodeBase64StringUtf8(data), }, }); + return res.data.signedBlob; } } diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index c5ecf5b2..9a12f509 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -404,10 +404,77 @@ export interface VerifyIdTokenOptions { maxExpiry?: number; } +export interface OAuth2ClientEndpoints { + /** + * The endpoint for viewing access token information + * + * @example + * 'https://oauth2.googleapis.com/tokeninfo' + */ + tokenInfoUrl: string | URL; + + /** + * The base URL for auth endpoints. + * + * @example + * 'https://accounts.google.com/o/oauth2/v2/auth' + */ + oauth2AuthBaseUrl: string | URL; + + /** + * The base endpoint for token retrieval + * . + * @example + * 'https://oauth2.googleapis.com/token' + */ + oauth2TokenUrl: string | URL; + + /** + * The base endpoint to revoke tokens. + * + * @example + * 'https://oauth2.googleapis.com/revoke' + */ + oauth2RevokeUrl: string | URL; + + /** + * Sign on certificates in PEM format. + * + * @example + * 'https://www.googleapis.com/oauth2/v1/certs' + */ + oauth2FederatedSignonPemCertsUrl: string | URL; + + /** + * Sign on certificates in JWK format. + * + * @example + * 'https://www.googleapis.com/oauth2/v3/certs' + */ + oauth2FederatedSignonJwkCertsUrl: string | URL; + + /** + * IAP Public Key URL. + * This URL contains a JSON dictionary that maps the `kid` claims to the public key values. + * + * @example + * 'https://www.gstatic.com/iap/verify/public_key' + */ + oauth2IapPublicKeyUrl: string | URL; +} + export interface OAuth2ClientOptions extends AuthClientOptions { clientId?: string; clientSecret?: string; redirectUri?: string; + /** + * Customizable endpoints. + */ + endpoints?: Partial; + /** + * The allowed OAuth2 token issuers. + */ + issuers?: string[]; } // Re-exporting here for backwards compatibility @@ -422,6 +489,8 @@ export class OAuth2Client extends AuthClient { private certificateExpiry: Date | null = null; private certificateCacheFormat: CertificateFormat = CertificateFormat.PEM; protected refreshTokenPromises = new Map>(); + readonly endpoints: Readonly; + readonly issuers: string[]; // TODO: refactor tests to make this private _clientId?: string; @@ -460,46 +529,30 @@ export class OAuth2Client extends AuthClient { this._clientId = opts.clientId; this._clientSecret = opts.clientSecret; this.redirectUri = opts.redirectUri; - } - protected static readonly GOOGLE_TOKEN_INFO_URL = - 'https://oauth2.googleapis.com/tokeninfo'; - - /** - * The base URL for auth endpoints. - */ - private static readonly GOOGLE_OAUTH2_AUTH_BASE_URL_ = - 'https://accounts.google.com/o/oauth2/v2/auth'; - - /** - * The base endpoint for token retrieval. - */ - private static readonly GOOGLE_OAUTH2_TOKEN_URL_ = - 'https://oauth2.googleapis.com/token'; - - /** - * The base endpoint to revoke tokens. - */ - private static readonly GOOGLE_OAUTH2_REVOKE_URL_ = - 'https://oauth2.googleapis.com/revoke'; - - /** - * Google Sign on certificates in PEM format. - */ - private static readonly GOOGLE_OAUTH2_FEDERATED_SIGNON_PEM_CERTS_URL_ = - 'https://www.googleapis.com/oauth2/v1/certs'; + this.endpoints = { + tokenInfoUrl: `https://oauth2.${this.universeDomain}/tokeninfo`, + oauth2AuthBaseUrl: 'https://accounts.google.com/o/oauth2/v2/auth', + oauth2TokenUrl: `https://oauth2.${this.universeDomain}/token`, + oauth2RevokeUrl: `https://oauth2.${this.universeDomain}/revoke`, + oauth2FederatedSignonPemCertsUrl: `https://www.${this.universeDomain}/oauth2/v1/certs`, + oauth2FederatedSignonJwkCertsUrl: `https://www.${this.universeDomain}/oauth2/v3/certs`, + oauth2IapPublicKeyUrl: 'https://www.gstatic.com/iap/verify/public_key', + ...opts.endpoints, + }; - /** - * Google Sign on certificates in JWK format. - */ - private static readonly GOOGLE_OAUTH2_FEDERATED_SIGNON_JWK_CERTS_URL_ = - 'https://www.googleapis.com/oauth2/v3/certs'; + this.issuers = opts.issuers || [ + 'accounts.google.com', + 'https://accounts.google.com', + this.universeDomain, + ]; + } /** - * Google Sign on certificates in JWK format. + * @deprecated use instance's {@link OAuth2Client.endpoints} */ - private static readonly GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_ = - 'https://www.gstatic.com/iap/verify/public_key'; + protected static readonly GOOGLE_TOKEN_INFO_URL = + 'https://oauth2.googleapis.com/tokeninfo'; /** * Clock skew - five minutes in seconds @@ -507,17 +560,9 @@ export class OAuth2Client extends AuthClient { private static readonly CLOCK_SKEW_SECS_ = 300; /** - * Max Token Lifetime is one day in seconds + * The default max Token Lifetime is one day in seconds */ - private static readonly MAX_TOKEN_LIFETIME_SECS_ = 86400; - - /** - * The allowed oauth token issuers. - */ - private static readonly ISSUERS_ = [ - 'accounts.google.com', - 'https://accounts.google.com', - ]; + private static readonly DEFAULT_MAX_TOKEN_LIFETIME_SECS_ = 86400; /** * Generates URL for consent page landing. @@ -537,7 +582,7 @@ export class OAuth2Client extends AuthClient { if (Array.isArray(opts.scope)) { opts.scope = opts.scope.join(' '); } - const rootUrl = OAuth2Client.GOOGLE_OAUTH2_AUTH_BASE_URL_; + const rootUrl = this.endpoints.oauth2AuthBaseUrl.toString(); return ( rootUrl + '?' + @@ -612,7 +657,7 @@ export class OAuth2Client extends AuthClient { private async getTokenAsync( options: GetTokenOptions ): Promise { - const url = OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_; + const url = this.endpoints.oauth2TokenUrl.toString(); const values = { code: options.code, client_id: options.client_id || this._clientId, @@ -673,7 +718,7 @@ export class OAuth2Client extends AuthClient { if (!refreshToken) { throw new Error('No refresh token is set.'); } - const url = OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_; + const url = this.endpoints.oauth2TokenUrl.toString(); const data = { refresh_token: refreshToken, client_id: this._clientId, @@ -874,10 +919,24 @@ export class OAuth2Client extends AuthClient { /** * Generates an URL to revoke the given token. * @param token The existing token to be revoked. + * + * @deprecated use instance method {@link OAuth2Client.getRevokeTokenURL} */ static getRevokeTokenUrl(token: string): string { - const parameters = querystring.stringify({token}); - return `${OAuth2Client.GOOGLE_OAUTH2_REVOKE_URL_}?${parameters}`; + return new OAuth2Client().getRevokeTokenURL(token).toString(); + } + + /** + * Generates a URL to revoke the given token. + * + * @param token The existing token to be revoked. + */ + getRevokeTokenURL(token: string): URL { + const url = new URL(this.endpoints.oauth2RevokeUrl); + + url.searchParams.append('token', token); + + return url; } /** @@ -895,7 +954,7 @@ export class OAuth2Client extends AuthClient { callback?: BodyResponseCallback ): GaxiosPromise | void { const opts: GaxiosOptions = { - url: OAuth2Client.getRevokeTokenUrl(token), + url: this.getRevokeTokenURL(token).toString(), method: 'POST', }; if (callback) { @@ -1080,7 +1139,7 @@ export class OAuth2Client extends AuthClient { options.idToken, response.certs, options.audience, - OAuth2Client.ISSUERS_, + this.issuers, options.maxExpiry ); @@ -1101,7 +1160,7 @@ export class OAuth2Client extends AuthClient { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Bearer ${accessToken}`, }, - url: OAuth2Client.GOOGLE_TOKEN_INFO_URL, + url: this.endpoints.tokenInfoUrl.toString(), }); const info = Object.assign( { @@ -1152,10 +1211,10 @@ export class OAuth2Client extends AuthClient { let url: string; switch (format) { case CertificateFormat.PEM: - url = OAuth2Client.GOOGLE_OAUTH2_FEDERATED_SIGNON_PEM_CERTS_URL_; + url = this.endpoints.oauth2FederatedSignonPemCertsUrl.toString(); break; case CertificateFormat.JWK: - url = OAuth2Client.GOOGLE_OAUTH2_FEDERATED_SIGNON_JWK_CERTS_URL_; + url = this.endpoints.oauth2FederatedSignonJwkCertsUrl.toString(); break; default: throw new Error(`Unsupported certificate format ${format}`); @@ -1226,7 +1285,7 @@ export class OAuth2Client extends AuthClient { async getIapPublicKeysAsync(): Promise { let res: GaxiosResponse; - const url: string = OAuth2Client.GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_; + const url = this.endpoints.oauth2IapPublicKeyUrl.toString(); try { res = await this.transporter.request({url}); @@ -1269,7 +1328,7 @@ export class OAuth2Client extends AuthClient { const crypto = createCrypto(); if (!maxExpiry) { - maxExpiry = OAuth2Client.MAX_TOKEN_LIFETIME_SECS_; + maxExpiry = OAuth2Client.DEFAULT_MAX_TOKEN_LIFETIME_SECS_; } const segments = jwt.split('.'); diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 44f431b7..a075eae1 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -140,7 +140,7 @@ export class StsCredentials extends OAuthClientAuthHandler { * available. */ constructor( - private readonly tokenExchangeEndpoint: string, + private readonly tokenExchangeEndpoint: string | URL, clientAuthentication?: ClientAuthentication ) { super(clientAuthentication); @@ -195,7 +195,7 @@ export class StsCredentials extends OAuthClientAuthHandler { Object.assign(headers, additionalHeaders || {}); const opts: GaxiosOptions = { - url: this.tokenExchangeEndpoint, + url: this.tokenExchangeEndpoint.toString(), method: 'POST', headers, data: querystring.stringify( diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 81d190ef..9cc62adf 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -61,7 +61,8 @@ const pkg = require('../../package.json'); export function mockStsTokenExchange( nockParams: NockMockStsToken[], - additionalHeaders?: {[key: string]: string} + additionalHeaders?: {[key: string]: string}, + baseURL = baseUrl ): nock.Scope { const headers = Object.assign( { @@ -69,7 +70,7 @@ export function mockStsTokenExchange( }, additionalHeaders || {} ); - const scope = nock(baseUrl, {reqheaders: headers}); + const scope = nock(baseURL, {reqheaders: headers}); nockParams.forEach(nockMockStsToken => { scope .post(path, qs.stringify(nockMockStsToken.request)) diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 96ea57ce..bfe5f680 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -171,43 +171,6 @@ describe('BaseExternalAccountClient', () => { }, expectedError); }); - const invalidWorkforceAudiences = [ - '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/provider', - '//iam.googleapis.com/locations/global/workforcepools/pool/providers/provider', - '//iam.googleapis.com/locations/global/workforcePools//providers/provider', - '//iam.googleapis.com/locations/global/workforcePools/providers/provider', - '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/provider', - '//iam.googleapis.com//locations/global/workforcePools/pool/providers/provider', - '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/provider', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers', - '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/provider', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', - '//iam.googleapis.com/locations//workforcePools/pool/providers/provider', - '//iam.googleapis.com/locations/workforcePools/pool/providers/provider', - '//iamAgoogleapisAcom/locations/global/workforcePools/workloadPools/providers/oidc', - ]; - const invalidExternalAccountOptionsWorkforceUserProject = Object.assign( - {}, - externalAccountOptionsWorkforceUserProject - ); - const expectedWorkforcePoolUserProjectError = new Error( - 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' - ); - - invalidWorkforceAudiences.forEach(invalidWorkforceAudience => { - it(`should throw given audience ${invalidWorkforceAudience} with user project defined in options`, () => { - invalidExternalAccountOptionsWorkforceUserProject.audience = - invalidWorkforceAudience; - - assert.throws(() => { - return new TestExternalAccountClient( - invalidExternalAccountOptionsWorkforceUserProject - ); - }, expectedWorkforcePoolUserProjectError); - }); - }); - it('should not throw on valid workforce audience configs', () => { const validWorkforceAudiences = [ '//iam.googleapis.com/locations/global/workforcePools/workforcePools/providers/provider', diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index d280a9ee..23baf404 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -325,6 +325,42 @@ describe('DownscopedClient', () => { refreshOptions.eagerRefreshThresholdMillis ); }); + + it('should use a tokenURL', async () => { + const tokenURL = new URL('https://my-token-url/v1/token'); + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ], + undefined, + tokenURL.origin + ); + + const downscopedClient = new DownscopedClient(client, { + ...testClientAccessBoundary, + tokenURL, + }); + + const tokenResponse = await downscopedClient.getAccessToken(); + assert.deepStrictEqual( + tokenResponse.token, + stsSuccessfulResponse.access_token + ); + + scope.done(); + }); }); describe('setCredential()', () => { diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index 4a083dff..30cf9013 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -93,21 +93,6 @@ describe('ExternalAccountClient', () => { forceRefreshOnFailure: true, }; - const invalidWorkforceIdentityPoolClientAudiences = [ - '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcepools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools//providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/providers/oidc', - '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/oidc', - '//iam.googleapis.com//locations/global/workforcePools/pool/providers/oidc', - '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', - '//iam.googleapis.com/locations//workforcePools/pool/providers/oidc', - '//iam.googleapis.com/locations/workforcePools/pool/providers/oidc', - ]; - it('should return IdentityPoolClient on IdentityPoolClientOptions', () => { const expectedClient = new IdentityPoolClient(fileSourcedOptions); @@ -201,29 +186,6 @@ describe('ExternalAccountClient', () => { ); }); - invalidWorkforceIdentityPoolClientAudiences.forEach( - invalidWorkforceIdentityPoolClientAudience => { - const workforceIdentityPoolClientInvalidOptions = Object.assign( - {}, - fileSourcedOptions, - { - workforce_pool_user_project: 'workforce_pool_user_project', - subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - } - ); - it(`should throw an error when an invalid workforce audience ${invalidWorkforceIdentityPoolClientAudience} is provided with a workforce user project`, () => { - workforceIdentityPoolClientInvalidOptions.audience = - invalidWorkforceIdentityPoolClientAudience; - - assert.throws(() => { - return ExternalAccountClient.fromJSON( - workforceIdentityPoolClientInvalidOptions - ); - }); - }); - } - ); - it('should return null when given non-ExternalAccountClientOptions', () => { assert( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index f7441567..0852c06c 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1404,8 +1404,10 @@ describe('googleauth', () => { ) .resolves(); + const universe = await auth.getUniverseDomain(); + const email = 'google@auth.library'; - const iamUri = 'https://iamcredentials.googleapis.com'; + const iamUri = `https://iamcredentials.${universe}`; const iamPath = `/v1/projects/-/serviceAccounts/${email}:signBlob`; const signedBlob = 'erutangis'; const data = 'abc123'; diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 1faa5bdd..a2bdcacb 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -179,29 +179,6 @@ describe('IdentityPoolClient', () => { }); describe('Constructor', () => { - const invalidWorkforceIdentityPoolClientAudiences = [ - '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcepools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools//providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/providers/oidc', - '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/oidc', - '//iam.googleapis.com//locations/global/workforcePools/pool/providers/oidc', - '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', - '//iam.googleapis.com/locations//workforcePools/pool/providers/oidc', - '//iam.googleapis.com/locations/workforcePools/pool/providers/oidc', - ]; - const invalidWorkforceIdentityPoolFileSourceOptions = Object.assign( - {}, - fileSourcedOptionsWithWorkforceUserProject - ); - const expectedWorkforcePoolUserProjectError = new Error( - 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' - ); - it('should throw when neither file or url sources are provided', () => { const expectedError = new Error( 'No valid Identity Pool "credential_source" provided, must be either file or url.' @@ -287,21 +264,6 @@ describe('IdentityPoolClient', () => { }, expectedError); }); - invalidWorkforceIdentityPoolClientAudiences.forEach( - invalidWorkforceIdentityPoolClientAudience => { - it(`should throw given audience ${invalidWorkforceIdentityPoolClientAudience} with user project defined in IdentityPoolClientOptions`, () => { - invalidWorkforceIdentityPoolFileSourceOptions.audience = - invalidWorkforceIdentityPoolClientAudience; - - assert.throws(() => { - return new IdentityPoolClient( - invalidWorkforceIdentityPoolFileSourceOptions - ); - }, expectedWorkforcePoolUserProjectError); - }); - } - ); - it('should not throw when valid file-sourced options are provided', () => { assert.doesNotThrow(() => { return new IdentityPoolClient(fileSourcedOptions); From 422de68d8d9ea66e6bf1fea923f61c8af0842420 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 26 Jan 2024 16:58:06 -0800 Subject: [PATCH 498/662] fix: Revert Missing `WORKFORCE_AUDIENCE_PATTERN` (#1740) --- src/auth/baseexternalclient.ts | 14 ++++++++++++ test/test.baseexternalclient.ts | 37 ++++++++++++++++++++++++++++++++ test/test.externalclient.ts | 38 +++++++++++++++++++++++++++++++++ test/test.identitypoolclient.ts | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 89bb610a..45ff17ff 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -60,6 +60,9 @@ export const EXTERNAL_ACCOUNT_TYPE = 'external_account'; **/ export const CLOUD_RESOURCE_MANAGER = 'https://cloudresourcemanager.googleapis.com/v1/projects/'; +/** The workforce audience pattern. */ +const WORKFORCE_AUDIENCE_PATTERN = + '//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); @@ -228,6 +231,17 @@ export abstract class BaseExternalAccountClient extends AuthClient { this.audience = opts.get('audience'); this.subjectTokenType = subjectTokenType; this.workforcePoolUserProject = workforcePoolUserProject; + const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN); + if ( + this.workforcePoolUserProject && + !this.audience.match(workforceAudiencePattern) + ) { + throw new Error( + 'workforcePoolUserProject should not be set for non-workforce pool ' + + 'credentials.' + ); + } + this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl; this.serviceAccountImpersonationLifetime = serviceAccountImpersonationLifetime; diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index bfe5f680..96ea57ce 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -171,6 +171,43 @@ describe('BaseExternalAccountClient', () => { }, expectedError); }); + const invalidWorkforceAudiences = [ + '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcepools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools//providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/providers/provider', + '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/provider', + '//iam.googleapis.com//locations/global/workforcePools/pool/providers/provider', + '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers', + '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/provider', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', + '//iam.googleapis.com/locations//workforcePools/pool/providers/provider', + '//iam.googleapis.com/locations/workforcePools/pool/providers/provider', + '//iamAgoogleapisAcom/locations/global/workforcePools/workloadPools/providers/oidc', + ]; + const invalidExternalAccountOptionsWorkforceUserProject = Object.assign( + {}, + externalAccountOptionsWorkforceUserProject + ); + const expectedWorkforcePoolUserProjectError = new Error( + 'workforcePoolUserProject should not be set for non-workforce pool ' + + 'credentials.' + ); + + invalidWorkforceAudiences.forEach(invalidWorkforceAudience => { + it(`should throw given audience ${invalidWorkforceAudience} with user project defined in options`, () => { + invalidExternalAccountOptionsWorkforceUserProject.audience = + invalidWorkforceAudience; + + assert.throws(() => { + return new TestExternalAccountClient( + invalidExternalAccountOptionsWorkforceUserProject + ); + }, expectedWorkforcePoolUserProjectError); + }); + }); + it('should not throw on valid workforce audience configs', () => { const validWorkforceAudiences = [ '//iam.googleapis.com/locations/global/workforcePools/workforcePools/providers/provider', diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index 30cf9013..4a083dff 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -93,6 +93,21 @@ describe('ExternalAccountClient', () => { forceRefreshOnFailure: true, }; + const invalidWorkforceIdentityPoolClientAudiences = [ + '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcepools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools//providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/providers/oidc', + '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/oidc', + '//iam.googleapis.com//locations/global/workforcePools/pool/providers/oidc', + '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', + '//iam.googleapis.com/locations//workforcePools/pool/providers/oidc', + '//iam.googleapis.com/locations/workforcePools/pool/providers/oidc', + ]; + it('should return IdentityPoolClient on IdentityPoolClientOptions', () => { const expectedClient = new IdentityPoolClient(fileSourcedOptions); @@ -186,6 +201,29 @@ describe('ExternalAccountClient', () => { ); }); + invalidWorkforceIdentityPoolClientAudiences.forEach( + invalidWorkforceIdentityPoolClientAudience => { + const workforceIdentityPoolClientInvalidOptions = Object.assign( + {}, + fileSourcedOptions, + { + workforce_pool_user_project: 'workforce_pool_user_project', + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + } + ); + it(`should throw an error when an invalid workforce audience ${invalidWorkforceIdentityPoolClientAudience} is provided with a workforce user project`, () => { + workforceIdentityPoolClientInvalidOptions.audience = + invalidWorkforceIdentityPoolClientAudience; + + assert.throws(() => { + return ExternalAccountClient.fromJSON( + workforceIdentityPoolClientInvalidOptions + ); + }); + }); + } + ); + it('should return null when given non-ExternalAccountClientOptions', () => { assert( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index a2bdcacb..1faa5bdd 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -179,6 +179,29 @@ describe('IdentityPoolClient', () => { }); describe('Constructor', () => { + const invalidWorkforceIdentityPoolClientAudiences = [ + '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcepools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools//providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/providers/oidc', + '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/oidc', + '//iam.googleapis.com//locations/global/workforcePools/pool/providers/oidc', + '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/oidc', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers', + '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', + '//iam.googleapis.com/locations//workforcePools/pool/providers/oidc', + '//iam.googleapis.com/locations/workforcePools/pool/providers/oidc', + ]; + const invalidWorkforceIdentityPoolFileSourceOptions = Object.assign( + {}, + fileSourcedOptionsWithWorkforceUserProject + ); + const expectedWorkforcePoolUserProjectError = new Error( + 'workforcePoolUserProject should not be set for non-workforce pool ' + + 'credentials.' + ); + it('should throw when neither file or url sources are provided', () => { const expectedError = new Error( 'No valid Identity Pool "credential_source" provided, must be either file or url.' @@ -264,6 +287,21 @@ describe('IdentityPoolClient', () => { }, expectedError); }); + invalidWorkforceIdentityPoolClientAudiences.forEach( + invalidWorkforceIdentityPoolClientAudience => { + it(`should throw given audience ${invalidWorkforceIdentityPoolClientAudience} with user project defined in IdentityPoolClientOptions`, () => { + invalidWorkforceIdentityPoolFileSourceOptions.audience = + invalidWorkforceIdentityPoolClientAudience; + + assert.throws(() => { + return new IdentityPoolClient( + invalidWorkforceIdentityPoolFileSourceOptions + ); + }, expectedWorkforcePoolUserProjectError); + }); + } + ); + it('should not throw when valid file-sourced options are provided', () => { assert.doesNotThrow(() => { return new IdentityPoolClient(fileSourcedOptions); From b51713c1186037ed72f18ee821769b4b354c7b76 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 29 Jan 2024 11:39:08 -0800 Subject: [PATCH 499/662] refactor: Consistency Between Langs (#1744) * refactor: Remove `tokenURL` * refactor: Revert to `googleapis.com` --- src/auth/downscopedclient.ts | 14 +------------- src/auth/oauth2client.ts | 12 +++++++----- test/test.downscopedclient.ts | 36 ----------------------------------- 3 files changed, 8 insertions(+), 54 deletions(-) diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index b4a7a019..2e2716f1 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -73,13 +73,6 @@ export interface CredentialAccessBoundary { accessBoundary: { accessBoundaryRules: AccessBoundaryRule[]; }; - /** - * An optional STS access token exchange endpoint. - * - * @example - * 'https://sts.googleapis.com/v1/token' - */ - tokenURL?: string | URL; } /** Defines an upper bound of permissions on a particular resource. */ @@ -141,11 +134,6 @@ export class DownscopedClient extends AuthClient { ) { super({...additionalOptions, quotaProjectId}); - // extract and remove `tokenURL` as it is not officially a part of the credentialAccessBoundary - this.credentialAccessBoundary = {...credentialAccessBoundary}; - const tokenURL = this.credentialAccessBoundary.tokenURL; - delete this.credentialAccessBoundary.tokenURL; - // Check 1-10 Access Boundary Rules are defined within Credential Access // Boundary. if ( @@ -174,7 +162,7 @@ export class DownscopedClient extends AuthClient { } this.stsCredential = new sts.StsCredentials( - tokenURL || `https://sts.${this.universeDomain}/v1/token` + `https://sts.${this.universeDomain}/v1/token` ); this.cachedDownscopedAccessToken = null; diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 9a12f509..e5a94c31 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -531,12 +531,14 @@ export class OAuth2Client extends AuthClient { this.redirectUri = opts.redirectUri; this.endpoints = { - tokenInfoUrl: `https://oauth2.${this.universeDomain}/tokeninfo`, + tokenInfoUrl: 'https://oauth2.googleapis.com/tokeninfo', oauth2AuthBaseUrl: 'https://accounts.google.com/o/oauth2/v2/auth', - oauth2TokenUrl: `https://oauth2.${this.universeDomain}/token`, - oauth2RevokeUrl: `https://oauth2.${this.universeDomain}/revoke`, - oauth2FederatedSignonPemCertsUrl: `https://www.${this.universeDomain}/oauth2/v1/certs`, - oauth2FederatedSignonJwkCertsUrl: `https://www.${this.universeDomain}/oauth2/v3/certs`, + oauth2TokenUrl: 'https://oauth2.googleapis.com/token', + oauth2RevokeUrl: 'https://oauth2.googleapis.com/revoke', + oauth2FederatedSignonPemCertsUrl: + 'https://www.googleapis.com/oauth2/v1/certs', + oauth2FederatedSignonJwkCertsUrl: + 'https://www.googleapis.com/oauth2/v3/certs', oauth2IapPublicKeyUrl: 'https://www.gstatic.com/iap/verify/public_key', ...opts.endpoints, }; diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index 23baf404..d280a9ee 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -325,42 +325,6 @@ describe('DownscopedClient', () => { refreshOptions.eagerRefreshThresholdMillis ); }); - - it('should use a tokenURL', async () => { - const tokenURL = new URL('https://my-token-url/v1/token'); - const scope = mockStsTokenExchange( - [ - { - statusCode: 200, - response: stsSuccessfulResponse, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token_0', - subject_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - options: JSON.stringify(testClientAccessBoundary), - }, - }, - ], - undefined, - tokenURL.origin - ); - - const downscopedClient = new DownscopedClient(client, { - ...testClientAccessBoundary, - tokenURL, - }); - - const tokenResponse = await downscopedClient.getAccessToken(); - assert.deepStrictEqual( - tokenResponse.token, - stsSuccessfulResponse.access_token - ); - - scope.done(); - }); }); describe('setCredential()', () => { From f79a1da6486dec13adce9e7fb5d1695413c09c72 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:51:35 -0800 Subject: [PATCH 500/662] chore(main): release 9.6.0 (#1733) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f675ea..3527420b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.6.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.5.0...v9.6.0) (2024-01-29) + + +### Features + +* Open More Endpoints for Customization ([#1721](https://github.com/googleapis/google-auth-library-nodejs/issues/1721)) ([effbf87](https://github.com/googleapis/google-auth-library-nodejs/commit/effbf87f6f0fd11a0cb1c749dad81737926dc436)) +* Use self-signed JWTs when non-default Universe Domains ([#1722](https://github.com/googleapis/google-auth-library-nodejs/issues/1722)) ([7e9876e](https://github.com/googleapis/google-auth-library-nodejs/commit/7e9876e2496b073220ca270368da7e9522da88f9)) + + +### Bug Fixes + +* Revert Missing `WORKFORCE_AUDIENCE_PATTERN` ([#1740](https://github.com/googleapis/google-auth-library-nodejs/issues/1740)) ([422de68](https://github.com/googleapis/google-auth-library-nodejs/commit/422de68d8d9ea66e6bf1fea923f61c8af0842420)) + ## [9.5.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.4.2...v9.5.0) (2024-01-25) diff --git a/package.json b/package.json index 663c658a..f1202a63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.5.0", + "version": "9.6.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index ab509181..382f6470 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^14.0.0", - "google-auth-library": "^9.5.0", + "google-auth-library": "^9.6.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 7282af878b9c5da17d00b23f99d2f60093b313fb Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 29 Jan 2024 13:06:16 -0800 Subject: [PATCH 501/662] test: Fix TPC Tests When Running on GCP (#1741) --- test/test.googleauth.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 0852c06c..93609af7 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -53,7 +53,7 @@ import { saEmail, } from './externalclienthelper'; import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; -import {AuthClient} from '../src/auth/authclient'; +import {AuthClient, DEFAULT_UNIVERSE} from '../src/auth/authclient'; import {ExternalAccountAuthorizedUserClient} from '../src/auth/externalAccountAuthorizedUserClient'; nock.disableNetConnect(); @@ -1386,6 +1386,7 @@ describe('googleauth', () => { client_email: 'google@auth.library', private_key: privateKey, }, + universeDomain: DEFAULT_UNIVERSE, }); const value = await auth.sign(data); const sign = crypto.createSign('RSA-SHA256'); @@ -1605,7 +1606,9 @@ describe('googleauth', () => { // Set up a mock to explicity return the Project ID, as needed for impersonated ADC mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); - const auth = new GoogleAuth(); + const auth = new GoogleAuth({ + universeDomain: DEFAULT_UNIVERSE, + }); const client = await auth.getClient(); const email = 'target@project.iam.gserviceaccount.com'; @@ -2309,6 +2312,7 @@ describe('googleauth', () => { it('should reject when no impersonation is used', async () => { const auth = new GoogleAuth({ credentials: createExternalAccountJSON(), + universeDomain: DEFAULT_UNIVERSE, }); await assert.rejects( @@ -2347,7 +2351,10 @@ describe('googleauth', () => { ) .reply(200, {signedBlob}) ); - const auth = new GoogleAuth({credentials: configWithImpersonation}); + const auth = new GoogleAuth({ + credentials: configWithImpersonation, + universeDomain: DEFAULT_UNIVERSE, + }); const value = await auth.sign(data); @@ -2357,7 +2364,10 @@ describe('googleauth', () => { }); it('getIdTokenClient() should reject', async () => { - const auth = new GoogleAuth({credentials: createExternalAccountJSON()}); + const auth = new GoogleAuth({ + credentials: createExternalAccountJSON(), + universeDomain: DEFAULT_UNIVERSE, + }); await assert.rejects( auth.getIdTokenClient('a-target-audience'), @@ -2536,6 +2546,7 @@ describe('googleauth', () => { it('should reject', async () => { const auth = new GoogleAuth({ credentials: createExternalAccountAuthorizedUserJson(), + universeDomain: DEFAULT_UNIVERSE, }); await assert.rejects( From a4f9f9c65853a37e6e83861c5d22533dba774037 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 31 Jan 2024 16:41:00 -0800 Subject: [PATCH 502/662] fix: Universe Domain Resolution (#1745) * fix: Universe Domain Resolution * fix: `source_credentials` from JSON * test: Add ADC universe domain test --- src/auth/googleauth.ts | 47 ++++++++++++++++++++----------------- src/auth/jwtclient.ts | 1 + src/auth/refreshclient.ts | 1 + test/fixtures/private2.json | 5 ++-- test/test.googleauth.ts | 14 +++++++++++ 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index b4f6cf15..aca320ac 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -185,13 +185,6 @@ export class GoogleAuth { private scopes?: string | string[]; private clientOptions: AuthClientOptions = {}; - /** - * The cached universe domain. - * - * @see {@link GoogleAuth.getUniverseDomain} - */ - #universeDomain?: string = undefined; - /** * Export DefaultTransporter as a static property of the class. */ @@ -220,7 +213,6 @@ export class GoogleAuth { if (opts.universeDomain) { this.clientOptions.universeDomain = opts.universeDomain; - this.#universeDomain = opts.universeDomain; } } @@ -315,9 +307,13 @@ export class GoogleAuth { return this._findProjectIdPromise; } - async #getUniverseFromMetadataServer() { - if (!(await this._checkIsGCE())) return; - + /** + * Retrieves a universe domain from the metadata server via + * {@link gcpMetadata.universe}. + * + * @returns a universe domain + */ + async getUniverseDomainFromMetadataServer(): Promise { let universeDomain: string; try { @@ -338,17 +334,18 @@ export class GoogleAuth { * Retrieves, caches, and returns the universe domain in the following order * of precedence: * - The universe domain in {@link GoogleAuth.clientOptions} - * - {@link gcpMetadata.universe} + * - An existing or ADC {@link AuthClient}'s universe domain + * - {@link gcpMetadata.universe}, if {@link Compute} client * * @returns The universe domain */ async getUniverseDomain(): Promise { - this.#universeDomain ??= originalOrCamelOptions(this.clientOptions).get( + let universeDomain = originalOrCamelOptions(this.clientOptions).get( 'universe_domain' ); - this.#universeDomain ??= await this.#getUniverseFromMetadataServer(); + universeDomain ??= (await this.getClient()).universeDomain; - return this.#universeDomain || DEFAULT_UNIVERSE; + return universeDomain; } /** @@ -438,7 +435,8 @@ export class GoogleAuth { if (await this._checkIsGCE()) { // set universe domain for Compute client if (!originalOrCamelOptions(options).get('universe_domain')) { - options.universeDomain = await this.getUniverseDomain(); + options.universeDomain = + await this.getUniverseDomainFromMetadataServer(); } (options as ComputeOptions).scopes = this.getAnyScopes(); @@ -622,11 +620,8 @@ export class GoogleAuth { } // Create source client for impersonation - const sourceClient = new UserRefreshClient( - json.source_credentials.client_id, - json.source_credentials.client_secret, - json.source_credentials.refresh_token - ); + const sourceClient = new UserRefreshClient(); + sourceClient.fromJSON(json.source_credentials); if (json.service_account_impersonation_url?.length > 256) { /** @@ -652,6 +647,7 @@ export class GoogleAuth { const targetScopes = this.getAnyScopes() ?? []; const client = new Impersonated({ + ...json, delegates: json.delegates ?? [], sourceClient: sourceClient, targetPrincipal: targetPrincipal, @@ -672,6 +668,10 @@ export class GoogleAuth { ): JSONClient { let client: JSONClient; + // user's preferred universe domain + const preferredUniverseDomain = + originalOrCamelOptions(options).get('universe_domain'); + if (json.type === USER_REFRESH_ACCOUNT_TYPE) { client = new UserRefreshClient(options); client.fromJSON(json); @@ -694,6 +694,11 @@ export class GoogleAuth { this.setGapicJWTValues(client); client.fromJSON(json); } + + if (preferredUniverseDomain) { + client.universeDomain = preferredUniverseDomain; + } + return client; } diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 9e12b23c..31c41dbd 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -321,6 +321,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { this.keyId = json.private_key_id; this.projectId = json.project_id; this.quotaProjectId = json.quota_project_id; + this.universeDomain = json.universe_domain || this.universeDomain; } /** diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 816be6e5..a53f1b1d 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -114,6 +114,7 @@ export class UserRefreshClient extends OAuth2Client { this._refreshToken = json.refresh_token; this.credentials.refresh_token = json.refresh_token; this.quotaProjectId = json.quota_project_id; + this.universeDomain = json.universe_domain || this.universeDomain; } /** diff --git a/test/fixtures/private2.json b/test/fixtures/private2.json index 88ab26e0..9aecf271 100644 --- a/test/fixtures/private2.json +++ b/test/fixtures/private2.json @@ -4,5 +4,6 @@ "client_email": "goodbye@youarecool.com", "client_id": "client456", "type": "service_account", - "project_id": "my-awesome-project" -} \ No newline at end of file + "project_id": "my-awesome-project", + "universe_domain": "my-universe" +} diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 93609af7..ff3d5bcd 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1568,6 +1568,20 @@ describe('googleauth', () => { assert.equal(await auth.getUniverseDomain(), universeDomain); }); + it('should get the universe from ADC', async () => { + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private2.json' + ); + const {universe_domain} = JSON.parse( + fs.readFileSync('./test/fixtures/private2.json', 'utf-8') + ); + + assert(universe_domain); + assert.notEqual(universe_domain, DEFAULT_UNIVERSE); + assert.equal(await auth.getUniverseDomain(), universe_domain); + }); + it('should use the metadata service if on GCP', async () => { const universeDomain = 'my.universe.com'; const scope = nockIsGCE({universeDomain}); From ecc6644d6a1c97c902de637142af7b21bda67667 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:42:24 -0800 Subject: [PATCH 503/662] chore(main): release 9.6.1 (#1746) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3527420b..c42ae065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.6.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.6.0...v9.6.1) (2024-02-01) + + +### Bug Fixes + +* Universe Domain Resolution ([#1745](https://github.com/googleapis/google-auth-library-nodejs/issues/1745)) ([a4f9f9c](https://github.com/googleapis/google-auth-library-nodejs/commit/a4f9f9c65853a37e6e83861c5d22533dba774037)) + ## [9.6.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.5.0...v9.6.0) (2024-01-29) diff --git a/package.json b/package.json index f1202a63..8e7814ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.6.0", + "version": "9.6.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 382f6470..f24f140a 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^14.0.0", - "google-auth-library": "^9.6.0", + "google-auth-library": "^9.6.1", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 696db72bb8644739768d20375d670813d4490714 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 2 Feb 2024 11:01:28 -0800 Subject: [PATCH 504/662] fix: Allow Get Universe Without Credentials (#1748) * fix: Allow Get Universe Without Credentials * chore: Pin `karma` --- package.json | 2 +- src/auth/googleauth.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8e7814ff..ac6ef892 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "karma-firefox-launcher": "^2.0.0", "karma-mocha": "^2.0.0", "karma-sourcemap-loader": "^0.4.0", - "karma-webpack": "^5.0.0", + "karma-webpack": "5.0.0", "keypair": "^1.0.4", "linkinator": "^4.0.0", "mocha": "^9.2.2", diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index aca320ac..472ae789 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -343,7 +343,12 @@ export class GoogleAuth { let universeDomain = originalOrCamelOptions(this.clientOptions).get( 'universe_domain' ); - universeDomain ??= (await this.getClient()).universeDomain; + try { + universeDomain ??= (await this.getClient()).universeDomain; + } catch { + // client or ADC is not available + universeDomain ??= DEFAULT_UNIVERSE; + } return universeDomain; } From 3ba07f5ff401fe330553a0860e765ecd3f80c77a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:09:25 -0800 Subject: [PATCH 505/662] chore(main): release 9.6.2 (#1749) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c42ae065..756cfe8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.6.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.6.1...v9.6.2) (2024-02-02) + + +### Bug Fixes + +* Allow Get Universe Without Credentials ([#1748](https://github.com/googleapis/google-auth-library-nodejs/issues/1748)) ([696db72](https://github.com/googleapis/google-auth-library-nodejs/commit/696db72bb8644739768d20375d670813d4490714)) + ## [9.6.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.6.0...v9.6.1) (2024-02-01) diff --git a/package.json b/package.json index ac6ef892..b9596558 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.6.1", + "version": "9.6.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index f24f140a..28ceaebb 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^14.0.0", - "google-auth-library": "^9.6.1", + "google-auth-library": "^9.6.2", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From f3d3a03dbce42a400c11457131dd1fabc206826a Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 6 Feb 2024 14:27:02 -0800 Subject: [PATCH 506/662] fix: Always sign with `scopes` on Non-Default Universes (#1752) --- src/auth/jwtclient.ts | 9 +++++++-- test/test.jwt.ts | 16 ++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 31c41dbd..a2d753d2 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -163,12 +163,17 @@ export class JWT extends OAuth2Client implements IdTokenProvider { scopes = this.defaultScopes; } + const useScopes = + this.useJWTAccessWithScope || + this.universeDomain !== DEFAULT_UNIVERSE; + const headers = await this.access.getRequestHeaders( url ?? undefined, this.additionalClaims, // Scopes take precedent over audience for signing, - // so we only provide them if useJWTAccessWithScope is on - this.useJWTAccessWithScope ? scopes : undefined + // so we only provide them if `useJWTAccessWithScope` is on or + // if we are in a non-default universe + useScopes ? scopes : undefined ); return {headers: this.addSharedMetadataHeaders(headers)}; diff --git a/test/test.jwt.ts b/test/test.jwt.ts index fc11bd02..79eb1605 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -896,7 +896,7 @@ describe('jwt', () => { ); }); - it('signs JWT with audience if: user scope = true, default scope = false, audience = falsy, useJWTAccessWithScope = true', async () => { + it('signs JWT with scopes if: user scope = true, default scope = false, audience = falsy, useJWTAccessWithScope = true', async () => { const stubGetRequestHeaders = sandbox.stub().returns({}); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, @@ -918,7 +918,7 @@ describe('jwt', () => { ); }); - it('signs JWT with audience if: user scope = false, default scope = true, audience = falsy, useJWTAccessWithScope = true', async () => { + it('signs JWT with scopes if: user scope = false, default scope = true, audience = falsy, useJWTAccessWithScope = true', async () => { const stubGetRequestHeaders = sandbox.stub().returns({}); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, @@ -939,7 +939,7 @@ describe('jwt', () => { ]); }); - it('signs JWT with audience if: user scope = true, default scope = true, audience = falsy, useJWTAccessWithScope = true', async () => { + it('signs JWT with scopes if: user scope = true, default scope = true, audience = falsy, useJWTAccessWithScope = true', async () => { const stubGetRequestHeaders = sandbox.stub().returns({}); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, @@ -962,7 +962,7 @@ describe('jwt', () => { ); }); - it('signs JWT with audience if: user scope = true, default scope = false, audience = truthy, useJWTAccessWithScope = true', async () => { + it('signs JWT with scopes if: user scope = true, default scope = false, audience = truthy, useJWTAccessWithScope = true', async () => { const stubGetRequestHeaders = sandbox.stub().returns({}); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, @@ -984,7 +984,7 @@ describe('jwt', () => { ); }); - it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true', async () => { + it('signs JWT with scopes if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true', async () => { const stubGetRequestHeaders = sandbox.stub().returns({}); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, @@ -1007,7 +1007,7 @@ describe('jwt', () => { ); }); - it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, universeDomain = not default universe', async () => { + it('signs JWT with scopes if: user scope = true, default scope = true, audience = truthy, universeDomain = not default universe', async () => { const stubGetRequestHeaders = sandbox.stub().returns({}); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, @@ -1025,11 +1025,11 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - undefined + ['scope1', 'scope2'] ); }); - it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true, universeDomain = not default universe', async () => { + it('signs JWT with scopes if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true, universeDomain = not default universe', async () => { const stubGetRequestHeaders = sandbox.stub().returns({}); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, From 3b19e9cfa0e7ca4ffd97fa0ebd96f065286573dc Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:50:44 -0800 Subject: [PATCH 507/662] chore(main): release 9.6.3 (#1753) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 756cfe8f..a9cf0b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.6.3](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.6.2...v9.6.3) (2024-02-06) + + +### Bug Fixes + +* Always sign with `scopes` on Non-Default Universes ([#1752](https://github.com/googleapis/google-auth-library-nodejs/issues/1752)) ([f3d3a03](https://github.com/googleapis/google-auth-library-nodejs/commit/f3d3a03dbce42a400c11457131dd1fabc206826a)) + ## [9.6.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.6.1...v9.6.2) (2024-02-02) diff --git a/package.json b/package.json index b9596558..939005c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.6.2", + "version": "9.6.3", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 28ceaebb..21e570f6 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^14.0.0", - "google-auth-library": "^9.6.2", + "google-auth-library": "^9.6.3", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From 40c847fcc4a3fa32b7f016599f8c43f9a7cbc686 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 26 Feb 2024 13:11:48 -0800 Subject: [PATCH 508/662] test: Log Error On Error (#1760) * test: Log Error On Error * chore: type fix * refactor: revert accidental changes from another branch --- samples/test/externalclient.test.js | 8 ++++---- src/auth/oauth2client.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js index b3e231c3..6f128529 100644 --- a/samples/test/externalclient.test.js +++ b/samples/test/externalclient.test.js @@ -290,7 +290,7 @@ describe('samples for external-account', () => { }, }); // Confirm expected script output. - assert.match(output, /DNS Info:/); + assert.match(output, /DNS Info:/, output); }); it('should sign the blobs with IAM credentials API', async () => { @@ -389,7 +389,7 @@ describe('samples for external-account', () => { }, }); // Confirm expected script output. - assert.match(output, /DNS Info:/); + assert.match(output, /DNS Info:/, output); }); it('should acquire ADC for AWS creds', async () => { @@ -427,7 +427,7 @@ describe('samples for external-account', () => { }, }); // Confirm expected script output. - assert.match(output, /DNS Info:/); + assert.match(output, /DNS Info:/, output); }); it('should acquire ADC for PluggableAuth creds', async () => { @@ -474,7 +474,7 @@ describe('samples for external-account', () => { }, }); // Confirm expected script output. - assert.match(output, /DNS Info:/); + assert.match(output, /DNS Info:/, output); }); it('should acquire access token with service account impersonation options', async () => { diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index e5a94c31..c9f87d56 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -854,7 +854,7 @@ export class OAuth2Client extends AuthClient { protected async getRequestMetadataAsync( // eslint-disable-next-line @typescript-eslint/no-unused-vars - url?: string | null + url?: string | URL | null ): Promise { const thisCreds = this.credentials; if ( From 6a6e49634863f61487688724d0d20632e03f0299 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:07:14 -0800 Subject: [PATCH 509/662] fix: making aws request signer get a new session token each time security credentials are requested. (#1765) --- src/auth/awsclient.ts | 12 +++++------- test/test.awsclient.ts | 1 + 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index bd47c7c4..dc2e0eee 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -155,7 +155,7 @@ export class AwsClient extends BaseExternalAccountClient { // The credential config contains all the URLs by default but clients may be running this // where the metadata server is not available and returning the credentials through the environment. // Removing this check may break them. - if (this.shouldUseMetadataServer() && this.imdsV2SessionTokenUrl) { + if (!this.regionFromEnv && this.imdsV2SessionTokenUrl) { metadataHeaders['x-aws-ec2-metadata-token'] = await this.getImdsV2SessionToken(); } @@ -167,6 +167,10 @@ export class AwsClient extends BaseExternalAccountClient { if (this.securityCredentialsFromEnv) { return this.securityCredentialsFromEnv; } + if (this.imdsV2SessionTokenUrl) { + metadataHeaders['x-aws-ec2-metadata-token'] = + await this.getImdsV2SessionToken(); + } // Since the role on a VM can change, we don't need to cache it. const roleName = await this.getAwsRoleName(metadataHeaders); // Temporary credentials typically last for several hours. @@ -316,12 +320,6 @@ export class AwsClient extends BaseExternalAccountClient { return response.data; } - private shouldUseMetadataServer(): boolean { - // The metadata server must be used when either the AWS region or AWS security - // credentials cannot be retrieved through their defined environment variables. - return !this.regionFromEnv || !this.securityCredentialsFromEnv; - } - private get regionFromEnv(): string | null { // The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. // Only one is required. diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 9a99685c..800f670b 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -330,6 +330,7 @@ describe('AwsClient', () => { reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }) .put('/latest/api/token') + .twice() .reply(200, awsSessionToken) ); From 0003bee317dd8e99b553857edfffeb4a47a4af26 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 12 Mar 2024 12:25:43 -0700 Subject: [PATCH 510/662] feat: `PassThrough` AuthClient (#1771) * feat: `PassThrough` AuthClient * refactor: Expand Types --- src/auth/authclient.ts | 7 ++-- src/auth/passthrough.ts | 65 ++++++++++++++++++++++++++++++++++ src/index.ts | 1 + test/test.authclient.ts | 25 ++----------- test/test.passthroughclient.ts | 62 ++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 27 deletions(-) create mode 100644 src/auth/passthrough.ts create mode 100644 test/test.passthroughclient.ts diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 12033203..29e1602a 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -17,7 +17,7 @@ import {Gaxios, GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; import {DefaultTransporter, Transporter} from '../transporters'; import {Credentials} from './credentials'; -import {Headers} from './oauth2client'; +import {GetAccessTokenResponse, Headers} from './oauth2client'; import {OriginalAndCamel, originalOrCamelOptions} from '../util'; /** @@ -128,10 +128,7 @@ export interface CredentialsClient { * @return A promise that resolves with the current GCP access token * response. If the current credential is expired, a new one is retrieved. */ - getAccessToken(): Promise<{ - token?: string | null; - res?: GaxiosResponse | null; - }>; + getAccessToken(): Promise; /** * The main authentication interface. It takes an optional url which when diff --git a/src/auth/passthrough.ts b/src/auth/passthrough.ts new file mode 100644 index 00000000..bde50cba --- /dev/null +++ b/src/auth/passthrough.ts @@ -0,0 +1,65 @@ +// Copyright 2024 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. + +import {GaxiosOptions} from 'gaxios'; +import {AuthClient} from './authclient'; +import {GetAccessTokenResponse, Headers} from './oauth2client'; + +/** + * An AuthClient without any Authentication information. Useful for: + * - Anonymous access + * - Local Emulators + * - Testing Environments + * + */ +export class PassThroughClient extends AuthClient { + /** + * Creates a request without any authentication headers or checks. + * + * @remarks + * + * In testing environments it may be useful to change the provided + * {@link AuthClient.transporter} for any desired request overrides/handling. + * + * @param opts + * @returns The response of the request. + */ + async request(opts: GaxiosOptions) { + return this.transporter.request(opts); + } + + /** + * A required method of the base class. + * Always will return an empty object. + * + * @returns {} + */ + async getAccessToken(): Promise { + return {}; + } + + /** + * A required method of the base class. + * Always will return an empty object. + * + * @returns {} + */ + async getRequestHeaders(): Promise { + return {}; + } +} + +const a = new PassThroughClient(); + +a.getAccessToken(); diff --git a/src/index.ts b/src/index.ts index 5225dd53..77ad950a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,6 +68,7 @@ export { PluggableAuthClient, PluggableAuthClientOptions, } from './auth/pluggable-auth-client'; +export {PassThroughClient} from './auth/passthrough'; export {DefaultTransporter} from './transporters'; const auth = new GoogleAuth(); diff --git a/test/test.authclient.ts b/test/test.authclient.ts index 786d4d04..2faa0f15 100644 --- a/test/test.authclient.ts +++ b/test/test.authclient.ts @@ -14,29 +14,10 @@ import {strict as assert} from 'assert'; -import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; -import {AuthClient} from '../src'; -import {Headers} from '../src/auth/oauth2client'; +import {PassThroughClient} from '../src'; import {snakeToCamel} from '../src/util'; describe('AuthClient', () => { - class TestAuthClient extends AuthClient { - request(opts: GaxiosOptions): GaxiosPromise { - throw new Error('Method not implemented.'); - } - - getRequestHeaders(url?: string | undefined): Promise { - throw new Error('Method not implemented.'); - } - - getAccessToken(): Promise<{ - token?: string | null | undefined; - res?: GaxiosResponse | null | undefined; - }> { - throw new Error('Method not implemented.'); - } - } - it('should accept and normalize snake case options to camel case', () => { const expected = { project_id: 'my-projectId', @@ -49,11 +30,11 @@ describe('AuthClient', () => { const camelCased = snakeToCamel(key) as keyof typeof authClient; // assert snake cased input - let authClient = new TestAuthClient({[key]: value}); + let authClient = new PassThroughClient({[key]: value}); assert.equal(authClient[camelCased], value); // assert camel cased input - authClient = new TestAuthClient({[camelCased]: value}); + authClient = new PassThroughClient({[camelCased]: value}); assert.equal(authClient[camelCased], value); } }); diff --git a/test/test.passthroughclient.ts b/test/test.passthroughclient.ts new file mode 100644 index 00000000..19a15387 --- /dev/null +++ b/test/test.passthroughclient.ts @@ -0,0 +1,62 @@ +// Copyright 2024 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. + +import {strict as assert} from 'assert'; + +import * as nock from 'nock'; + +import {PassThroughClient} from '../src'; + +describe('AuthClient', () => { + before(async () => { + nock.disableNetConnect(); + }); + + afterEach(async () => { + nock.cleanAll(); + }); + + describe('#getAccessToken', () => { + it('should return an empty object', async () => { + const client = new PassThroughClient(); + const token = await client.getAccessToken(); + + assert.deepEqual(token, {}); + }); + }); + + describe('#getRequestHeaders', () => { + it('should return an empty object', async () => { + const client = new PassThroughClient(); + const token = await client.getRequestHeaders(); + + assert.deepEqual(token, {}); + }); + }); + + describe('#request', () => { + it('should return the expected response', async () => { + const url = 'https://google.com'; + const example = {test: 'payload'}; + const scope = nock(url).get('/').reply(200, example); + + const client = new PassThroughClient(); + const response = await client.request({url}); + + assert.deepEqual(response.data, example); + + scope.done(); + }); + }); +}); From f45f9753a7c83bc04616a1bdbaf687b3f38a17d2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 12 Mar 2024 20:34:04 +0100 Subject: [PATCH 511/662] fix(deps): update dependency @googleapis/iam to v15 (#1772) Co-authored-by: Daniel Bankhead --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 21e570f6..bbcf2e0f 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^14.0.0", + "@googleapis/iam": "^15.0.0", "google-auth-library": "^9.6.3", "node-fetch": "^2.3.0", "opn": "^5.3.0", From 9a8d15f694801c9500d0b8e62efa3791244c5732 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:33:41 -0700 Subject: [PATCH 512/662] chore(main): release 9.7.0 (#1767) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9cf0b0e..22516d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.7.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.6.3...v9.7.0) (2024-03-12) + + +### Features + +* `PassThrough` AuthClient ([#1771](https://github.com/googleapis/google-auth-library-nodejs/issues/1771)) ([0003bee](https://github.com/googleapis/google-auth-library-nodejs/commit/0003bee317dd8e99b553857edfffeb4a47a4af26)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v15 ([#1772](https://github.com/googleapis/google-auth-library-nodejs/issues/1772)) ([f45f975](https://github.com/googleapis/google-auth-library-nodejs/commit/f45f9753a7c83bc04616a1bdbaf687b3f38a17d2)) +* Making aws request signer get a new session token each time security credentials are requested. ([#1765](https://github.com/googleapis/google-auth-library-nodejs/issues/1765)) ([6a6e496](https://github.com/googleapis/google-auth-library-nodejs/commit/6a6e49634863f61487688724d0d20632e03f0299)) + ## [9.6.3](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.6.2...v9.6.3) (2024-02-06) diff --git a/package.json b/package.json index 939005c3..ce217c30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.6.3", + "version": "9.7.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index bbcf2e0f..4737562f 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^15.0.0", - "google-auth-library": "^9.6.3", + "google-auth-library": "^9.7.0", "node-fetch": "^2.3.0", "opn": "^5.3.0", "server-destroy": "^1.0.1" From fc8dfe9d373e30dd1bd06eb8cbb8b52e735b4d83 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 22 Mar 2024 18:03:41 +0100 Subject: [PATCH 513/662] fix(deps): update dependency opn to v6 (#1775) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 4737562f..cef144b3 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,7 +17,7 @@ "@googleapis/iam": "^15.0.0", "google-auth-library": "^9.7.0", "node-fetch": "^2.3.0", - "opn": "^5.3.0", + "opn": "^6.0.0", "server-destroy": "^1.0.1" }, "devDependencies": { From 847caa043946dc2fe1866314a9c8a0894f3ae394 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 27 Mar 2024 09:51:42 -0700 Subject: [PATCH 514/662] refactor(deps): `opn` -> `open` (#1778) * refactor(deps): `opn` -> `open` The package has been renamed * chore: Copyright headers * revert: Unrelated change * chore: lint fixes --- samples/oauth2-codeVerifier.js | 6 +++--- samples/oauth2.js | 6 +++--- samples/package.json | 2 +- samples/verifyIdToken.js | 6 +++--- src/auth/computeclient.ts | 4 ++-- src/transporters.ts | 5 ++--- test/test.downscopedclient.ts | 4 ++-- test/test.jwt.ts | 1 - test/test.oauth2common.ts | 30 ++++++++++++------------------ test/test.pluggableauthclient.ts | 2 +- test/test.transporters.ts | 4 ++-- 11 files changed, 31 insertions(+), 39 deletions(-) diff --git a/samples/oauth2-codeVerifier.js b/samples/oauth2-codeVerifier.js index 4ec9208d..d91b7fa1 100644 --- a/samples/oauth2-codeVerifier.js +++ b/samples/oauth2-codeVerifier.js @@ -1,4 +1,4 @@ -// Copyright 2017, Google, Inc. +// Copyright 2017 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 @@ -16,7 +16,7 @@ const {OAuth2Client} = require('google-auth-library'); const http = require('http'); const url = require('url'); -const opn = require('opn'); +const open = require('open'); const destroyer = require('server-destroy'); // Download your OAuth2 configuration from the Google Developer Console. @@ -95,7 +95,7 @@ async function getAuthenticatedClient() { }) .listen(3000, () => { // open the browser to the authorize url to start the workflow - opn(authorizeUrl, {wait: false}).then(cp => cp.unref()); + open(authorizeUrl, {wait: false}).then(cp => cp.unref()); }); destroyer(server); }); diff --git a/samples/oauth2.js b/samples/oauth2.js index cfded95a..5d177a5d 100644 --- a/samples/oauth2.js +++ b/samples/oauth2.js @@ -1,4 +1,4 @@ -// Copyright 2017, Google, Inc. +// Copyright 2017 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 @@ -16,7 +16,7 @@ const {OAuth2Client} = require('google-auth-library'); const http = require('http'); const url = require('url'); -const opn = require('opn'); +const open = require('open'); const destroyer = require('server-destroy'); // Download your OAuth2 configuration from the Google @@ -88,7 +88,7 @@ function getAuthenticatedClient() { }) .listen(3000, () => { // open the browser to the authorize url to start the workflow - opn(authorizeUrl, {wait: false}).then(cp => cp.unref()); + open(authorizeUrl, {wait: false}).then(cp => cp.unref()); }); destroyer(server); }); diff --git a/samples/package.json b/samples/package.json index cef144b3..7e30dad8 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,7 +17,7 @@ "@googleapis/iam": "^15.0.0", "google-auth-library": "^9.7.0", "node-fetch": "^2.3.0", - "opn": "^6.0.0", + "open": "^6.0.0", "server-destroy": "^1.0.1" }, "devDependencies": { diff --git a/samples/verifyIdToken.js b/samples/verifyIdToken.js index a722df3b..89269892 100644 --- a/samples/verifyIdToken.js +++ b/samples/verifyIdToken.js @@ -1,4 +1,4 @@ -// Copyright 2017, Google, Inc. +// Copyright 2017 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 @@ -16,7 +16,7 @@ const {OAuth2Client} = require('google-auth-library'); const http = require('http'); const url = require('url'); -const opn = require('opn'); +const open = require('open'); const destroyer = require('server-destroy'); // Download your OAuth2 configuration from the Google @@ -87,7 +87,7 @@ function getAuthenticatedClient() { }) .listen(3000, () => { // open the browser to the authorize url to start the workflow - opn(authorizeUrl, {wait: false}).then(cp => cp.unref()); + open(authorizeUrl, {wait: false}).then(cp => cp.unref()); }); destroyer(server); }); diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 35626b0a..fbcec074 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -55,8 +55,8 @@ export class Compute extends OAuth2Client { this.scopes = Array.isArray(options.scopes) ? options.scopes : options.scopes - ? [options.scopes] - : []; + ? [options.scopes] + : []; } /** diff --git a/src/transporters.ts b/src/transporters.ts index d4a298f0..41660308 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -64,9 +64,8 @@ export class DefaultTransporter implements Transporter { if (!uaValue) { opts.headers['User-Agent'] = DefaultTransporter.USER_AGENT; } else if (!uaValue.includes(`${PRODUCT_NAME}/`)) { - opts.headers[ - 'User-Agent' - ] = `${uaValue} ${DefaultTransporter.USER_AGENT}`; + opts.headers['User-Agent'] = + `${uaValue} ${DefaultTransporter.USER_AGENT}`; } // track google-auth-library-nodejs version: if (!opts.headers['x-goog-api-client']) { diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index d280a9ee..b808be72 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -54,11 +54,11 @@ class TestAuthClient extends AuthClient { this.credentials.expiry_date = expirationTime; } - async getRequestHeaders(url?: string): Promise { + async getRequestHeaders(): Promise { throw new Error('Not implemented.'); } - request(opts: GaxiosOptions): GaxiosPromise { + request(): GaxiosPromise { throw new Error('Not implemented.'); } } diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 79eb1605..13b4695e 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -28,7 +28,6 @@ describe('jwt', () => { const keypair = require('keypair'); const PEM_PATH = './test/fixtures/private.pem'; const PEM_CONTENTS = fs.readFileSync(PEM_PATH, 'utf8'); - const P12_PATH = './test/fixtures/key.p12'; nock.disableNetConnect(); diff --git a/test/test.oauth2common.ts b/test/test.oauth2common.ts index 9b0a485f..2afd159a 100644 --- a/test/test.oauth2common.ts +++ b/test/test.oauth2common.ts @@ -104,9 +104,8 @@ describe('OAuthClientAuthHandler', () => { }; const actualOptions = Object.assign({}, originalOptions); const expectedOptions = Object.assign({}, originalOptions); - ( - expectedOptions.headers as Headers - ).Authorization = `Basic ${expectedBase64EncodedCred}`; + (expectedOptions.headers as Headers).Authorization = + `Basic ${expectedBase64EncodedCred}`; handler.testApplyClientAuthenticationOptions(actualOptions); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -127,9 +126,8 @@ describe('OAuthClientAuthHandler', () => { }; const actualOptions = Object.assign({}, originalOptions); const expectedOptions = Object.assign({}, originalOptions); - ( - expectedOptions.headers as Headers - ).Authorization = `Basic ${expectedBase64EncodedCredNoSecret}`; + (expectedOptions.headers as Headers).Authorization = + `Basic ${expectedBase64EncodedCredNoSecret}`; handler.testApplyClientAuthenticationOptions(actualOptions); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -146,9 +144,8 @@ describe('OAuthClientAuthHandler', () => { }; const actualOptions = Object.assign({}, originalOptions); const expectedOptions = Object.assign({}, originalOptions); - ( - expectedOptions.headers as Headers - ).Authorization = `Basic ${expectedBase64EncodedCred}`; + (expectedOptions.headers as Headers).Authorization = + `Basic ${expectedBase64EncodedCred}`; handler.testApplyClientAuthenticationOptions(actualOptions); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -349,9 +346,8 @@ describe('OAuthClientAuthHandler', () => { }; const actualOptions = Object.assign({}, originalOptions); const expectedOptions = Object.assign({}, originalOptions); - ( - expectedOptions.headers as Headers - ).Authorization = `Bearer ${bearerToken}`; + (expectedOptions.headers as Headers).Authorization = + `Bearer ${bearerToken}`; handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -374,9 +370,8 @@ describe('OAuthClientAuthHandler', () => { const actualOptions = Object.assign({}, originalOptions); // Expected options should have bearer token in header. const expectedOptions = Object.assign({}, originalOptions); - ( - expectedOptions.headers as Headers - ).Authorization = `Bearer ${bearerToken}`; + (expectedOptions.headers as Headers).Authorization = + `Bearer ${bearerToken}`; handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); assert.deepStrictEqual(expectedOptions, actualOptions); @@ -399,9 +394,8 @@ describe('OAuthClientAuthHandler', () => { const actualOptions = Object.assign({}, originalOptions); // Expected options should have bearer token in header. const expectedOptions = Object.assign({}, originalOptions); - ( - expectedOptions.headers as Headers - ).Authorization = `Bearer ${bearerToken}`; + (expectedOptions.headers as Headers).Authorization = + `Bearer ${bearerToken}`; handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); assert.deepStrictEqual(expectedOptions, actualOptions); diff --git a/test/test.pluggableauthclient.ts b/test/test.pluggableauthclient.ts index 17ecf351..45cbaf8d 100644 --- a/test/test.pluggableauthclient.ts +++ b/test/test.pluggableauthclient.ts @@ -17,7 +17,7 @@ import { ExecutableError, PluggableAuthClient, } from '../src/auth/pluggable-auth-client'; -import {BaseExternalAccountClient, IdentityPoolClient} from '../src'; +import {BaseExternalAccountClient} from '../src'; import { assertGaxiosResponsePresent, getAudience, diff --git a/test/test.transporters.ts b/test/test.transporters.ts index d7da2196..6c019191 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -155,9 +155,9 @@ describe('transporters', () => { assert.strictEqual(res!.status, 200); done(); }, - _error => { + error => { scope.done(); - done('Unexpected promise failure'); + done(error); } ); }); From 16e5cae1d56d5c3dd6cc3bdca5ecdfb534eaf529 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 29 Mar 2024 21:30:16 +0100 Subject: [PATCH 515/662] fix(deps): update dependency open to v10 (#1782) * fix(deps): update dependency open to v10 * chore: Use v9 --------- Co-authored-by: Daniel Bankhead --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 7e30dad8..540f12c9 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,7 +17,7 @@ "@googleapis/iam": "^15.0.0", "google-auth-library": "^9.7.0", "node-fetch": "^2.3.0", - "open": "^6.0.0", + "open": "^9.0.0", "server-destroy": "^1.0.1" }, "devDependencies": { From 9b69a3119c2d0dfe12d41a5f77658d35a2c92d74 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 12 Apr 2024 14:01:06 -0700 Subject: [PATCH 516/662] feat: Enable Retries For Auth Requests (#1791) * feat: Enable Retries For Auth Requests * chore: lint * docs: `reAuthRetried` clarification * chore: lint * refactor: Use `IdentityPoolClient.RETRY_CONFIG` --- src/auth/authclient.ts | 18 +++++++++++++ src/auth/awsclient.ts | 5 ++++ src/auth/baseexternalclient.ts | 8 +++--- src/auth/downscopedclient.ts | 6 ++--- .../externalAccountAuthorizedUserClient.ts | 7 ++--- src/auth/googleauth.ts | 4 +++ src/auth/identitypoolclient.ts | 1 + src/auth/impersonated.ts | 8 +++--- src/auth/oauth2client.ts | 25 +++++++++++++---- src/auth/oauth2common.ts | 18 +++++++++++++ src/auth/stscredentials.ts | 1 + test/test.awsclient.ts | 20 +++++++------- test/test.downscopedclient.ts | 2 +- ...est.externalaccountauthorizeduserclient.ts | 17 +++++++++--- test/test.jwt.ts | 15 +++++++---- test/test.jwtaccess.ts | 15 +++++++---- test/test.oauth2.ts | 4 +-- test/test.stscredentials.ts | 27 +++++++++++-------- 18 files changed, 145 insertions(+), 56 deletions(-) diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 29e1602a..8e004d63 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -259,4 +259,22 @@ export abstract class AuthClient } return headers; } + + /** + * Retry config for Auth-related requests. + * + * @remarks + * + * This is not a part of the default {@link AuthClient.transporter transporter/gaxios} + * config as some downstream APIs would prefer if customers explicitly enable retries, + * such as GCS. + */ + protected static get RETRY_CONFIG(): GaxiosOptions { + return { + retry: true, + retryConfig: { + httpMethodsToRetry: ['GET', 'PUT', 'POST', 'HEAD', 'OPTIONS', 'DELETE'], + }, + }; + } } diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index dc2e0eee..f9f899d9 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -192,6 +192,7 @@ export class AwsClient extends BaseExternalAccountClient { // Generate signed request to AWS STS GetCallerIdentity API. // Use the required regional endpoint. Otherwise, the request will fail. const options = await this.awsRequestSigner.getRequestOptions({ + ...AwsClient.RETRY_CONFIG, url: this.regionalCredVerificationUrl.replace('{region}', this.region), method: 'POST', }); @@ -240,6 +241,7 @@ export class AwsClient extends BaseExternalAccountClient { */ private async getImdsV2SessionToken(): Promise { const opts: GaxiosOptions = { + ...AwsClient.RETRY_CONFIG, url: this.imdsV2SessionTokenUrl, method: 'PUT', responseType: 'text', @@ -266,6 +268,7 @@ export class AwsClient extends BaseExternalAccountClient { ); } const opts: GaxiosOptions = { + ...AwsClient.RETRY_CONFIG, url: this.regionUrl, method: 'GET', responseType: 'text', @@ -290,6 +293,7 @@ export class AwsClient extends BaseExternalAccountClient { ); } const opts: GaxiosOptions = { + ...AwsClient.RETRY_CONFIG, url: this.securityCredentialsUrl, method: 'GET', responseType: 'text', @@ -313,6 +317,7 @@ export class AwsClient extends BaseExternalAccountClient { ): Promise { const response = await this.transporter.request({ + ...AwsClient.RETRY_CONFIG, url: `${this.securityCredentialsUrl}/${roleName}`, responseType: 'json', headers: headers, diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 45ff17ff..94789556 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -381,6 +381,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { // Preferable not to use request() to avoid retrial policies. const headers = await this.getRequestHeaders(); const response = await this.transporter.request({ + ...BaseExternalAccountClient.RETRY_CONFIG, headers, url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`, responseType: 'json', @@ -395,12 +396,12 @@ export abstract class BaseExternalAccountClient extends AuthClient { * Authenticates the provided HTTP request, processes it and resolves with the * returned response. * @param opts The HTTP request options. - * @param retry Whether the current attempt is a retry after a failed attempt. + * @param reAuthRetried Whether the current attempt is a retry after a failed attempt due to an auth failure. * @return A promise that resolves with the successful response. */ protected async requestAsync( opts: GaxiosOptions, - retry = false + reAuthRetried = false ): Promise> { let response: GaxiosResponse; try { @@ -426,7 +427,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { const isReadableStream = res.config.data instanceof stream.Readable; const isAuthErr = statusCode === 401 || statusCode === 403; if ( - !retry && + !reAuthRetried && isAuthErr && !isReadableStream && this.forceRefreshOnFailure @@ -554,6 +555,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { token: string ): Promise { const opts: GaxiosOptions = { + ...BaseExternalAccountClient.RETRY_CONFIG, url: this.serviceAccountImpersonationUrl!, method: 'POST', headers: { diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 2e2716f1..3005e7cc 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -250,12 +250,12 @@ export class DownscopedClient extends AuthClient { * Authenticates the provided HTTP request, processes it and resolves with the * returned response. * @param opts The HTTP request options. - * @param retry Whether the current attempt is a retry after a failed attempt. + * @param reAuthRetried Whether the current attempt is a retry after a failed attempt due to an auth failure * @return A promise that resolves with the successful response. */ protected async requestAsync( opts: GaxiosOptions, - retry = false + reAuthRetried = false ): Promise> { let response: GaxiosResponse; try { @@ -281,7 +281,7 @@ export class DownscopedClient extends AuthClient { const isReadableStream = res.config.data instanceof stream.Readable; const isAuthErr = statusCode === 401 || statusCode === 403; if ( - !retry && + !reAuthRetried && isAuthErr && !isReadableStream && this.forceRefreshOnFailure diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index c9534440..e338164c 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -113,6 +113,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { }; const opts: GaxiosOptions = { + ...ExternalAccountAuthorizedUserHandler.RETRY_CONFIG, url: this.url, method: 'POST', headers, @@ -248,12 +249,12 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { * Authenticates the provided HTTP request, processes it and resolves with the * returned response. * @param opts The HTTP request options. - * @param retry Whether the current attempt is a retry after a failed attempt. + * @param reAuthRetried Whether the current attempt is a retry after a failed attempt due to an auth failure. * @return A promise that resolves with the successful response. */ protected async requestAsync( opts: GaxiosOptions, - retry = false + reAuthRetried = false ): Promise> { let response: GaxiosResponse; try { @@ -279,7 +280,7 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { const isReadableStream = res.config.data instanceof stream.Readable; const isAuthErr = statusCode === 401 || statusCode === 403; if ( - !retry && + !reAuthRetried && isAuthErr && !isReadableStream && this.forceRefreshOnFailure diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 472ae789..f3faedac 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -1130,6 +1130,10 @@ export class GoogleAuth { data: { payload: crypto.encodeBase64StringUtf8(data), }, + retry: true, + retryConfig: { + httpMethodsToRetry: ['POST'], + }, }); return res.data.signedBlob; diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index 2ed66b11..0e89f544 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -225,6 +225,7 @@ export class IdentityPoolClient extends BaseExternalAccountClient { headers?: {[key: string]: string} ): Promise { const opts: GaxiosOptions = { + ...IdentityPoolClient.RETRY_CONFIG, url, method: 'GET', headers, diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index ac24fd7a..2b4a2555 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -143,6 +143,7 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { payload: Buffer.from(blobToSign).toString('base64'), }; const res = await this.sourceClient.request({ + ...Impersonated.RETRY_CONFIG, url: u, data: body, method: 'POST', @@ -157,11 +158,8 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { /** * Refreshes the access token. - * @param refreshToken Unused parameter */ - protected async refreshToken( - refreshToken?: string | null - ): Promise { + protected async refreshToken(): Promise { try { await this.sourceClient.getAccessToken(); const name = 'projects/-/serviceAccounts/' + this.targetPrincipal; @@ -172,6 +170,7 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { lifetime: this.lifetime + 's', }; const res = await this.sourceClient.request({ + ...Impersonated.RETRY_CONFIG, url: u, data: body, method: 'POST', @@ -227,6 +226,7 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { includeEmail: options?.includeEmail ?? true, }; const res = await this.sourceClient.request({ + ...Impersonated.RETRY_CONFIG, url: u, data: body, method: 'POST', diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index c9f87d56..ce1fb829 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -669,6 +669,7 @@ export class OAuth2Client extends AuthClient { code_verifier: options.codeVerifier, }; const res = await this.transporter.request({ + ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, data: querystring.stringify(values), @@ -733,6 +734,7 @@ export class OAuth2Client extends AuthClient { try { // request for new token res = await this.transporter.request({ + ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, data: querystring.stringify(data), @@ -956,6 +958,7 @@ export class OAuth2Client extends AuthClient { callback?: BodyResponseCallback ): GaxiosPromise | void { const opts: GaxiosOptions = { + ...OAuth2Client.RETRY_CONFIG, url: this.getRevokeTokenURL(token).toString(), method: 'POST', }; @@ -1024,7 +1027,7 @@ export class OAuth2Client extends AuthClient { protected async requestAsync( opts: GaxiosOptions, - retry = false + reAuthRetried = false ): Promise> { let r2: GaxiosResponse; try { @@ -1078,11 +1081,16 @@ export class OAuth2Client extends AuthClient { this.refreshHandler; const isReadableStream = res.config.data instanceof stream.Readable; const isAuthErr = statusCode === 401 || statusCode === 403; - if (!retry && isAuthErr && !isReadableStream && mayRequireRefresh) { + if ( + !reAuthRetried && + isAuthErr && + !isReadableStream && + mayRequireRefresh + ) { await this.refreshAccessTokenAsync(); return this.requestAsync(opts, true); } else if ( - !retry && + !reAuthRetried && isAuthErr && !isReadableStream && mayRequireRefreshWithNoRefreshToken @@ -1157,6 +1165,7 @@ export class OAuth2Client extends AuthClient { */ async getTokenInfo(accessToken: string): Promise { const {data} = await this.transporter.request({ + ...OAuth2Client.RETRY_CONFIG, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -1222,7 +1231,10 @@ export class OAuth2Client extends AuthClient { throw new Error(`Unsupported certificate format ${format}`); } try { - res = await this.transporter.request({url}); + res = await this.transporter.request({ + ...OAuth2Client.RETRY_CONFIG, + url, + }); } catch (e) { if (e instanceof Error) { e.message = `Failed to retrieve verification certificates: ${e.message}`; @@ -1290,7 +1302,10 @@ export class OAuth2Client extends AuthClient { const url = this.endpoints.oauth2IapPublicKeyUrl.toString(); try { - res = await this.transporter.request({url}); + res = await this.transporter.request({ + ...OAuth2Client.RETRY_CONFIG, + url, + }); } catch (e) { if (e instanceof Error) { e.message = `Failed to retrieve verification certificates: ${e.message}`; diff --git a/src/auth/oauth2common.ts b/src/auth/oauth2common.ts index 34d1bb6d..19f4fe8e 100644 --- a/src/auth/oauth2common.ts +++ b/src/auth/oauth2common.ts @@ -181,6 +181,24 @@ export abstract class OAuthClientAuthHandler { } } } + + /** + * Retry config for Auth-related requests. + * + * @remarks + * + * This is not a part of the default {@link AuthClient.transporter transporter/gaxios} + * config as some downstream APIs would prefer if customers explicitly enable retries, + * such as GCS. + */ + protected static get RETRY_CONFIG(): GaxiosOptions { + return { + retry: true, + retryConfig: { + httpMethodsToRetry: ['GET', 'PUT', 'POST', 'HEAD', 'OPTIONS', 'DELETE'], + }, + }; + } } /** diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index a075eae1..291da246 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -195,6 +195,7 @@ export class StsCredentials extends OAuthClientAuthHandler { Object.assign(headers, additionalHeaders || {}); const opts: GaxiosOptions = { + ...StsCredentials.RETRY_CONFIG, url: this.tokenExchangeEndpoint.toString(), method: 'POST', headers, diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 800f670b..fb00cadd 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -401,12 +401,12 @@ describe('AwsClient', () => { // Simulate error during region retrieval. const scope = nock(metadataBaseUrl) .get('/latest/meta-data/placement/availability-zone') - .reply(500); + .reply(400); const client = new AwsClient(awsOptions); await assert.rejects(client.retrieveSubjectToken(), { - status: 500, + status: 400, }); scope.done(); }); @@ -435,12 +435,12 @@ describe('AwsClient', () => { .get('/latest/meta-data/iam/security-credentials') .reply(200, awsRole) .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) - .reply(408); + .reply(404); const client = new AwsClient(awsOptions); await assert.rejects(client.retrieveSubjectToken(), { - status: 408, + status: 404, }); scope.done(); }); @@ -602,12 +602,12 @@ describe('AwsClient', () => { .get('/latest/meta-data/placement/availability-zone') .reply(200, `${awsRegion}b`) .get('/latest/meta-data/iam/security-credentials') - .reply(500); + .reply(400); const client = new AwsClient(awsOptions); await assert.rejects(client.getAccessToken(), { - status: 500, + status: 400, }); scope.done(); }); @@ -704,12 +704,12 @@ describe('AwsClient', () => { // Simulate error during region retrieval. const scope = nock(metadataBaseUrl) .get('/latest/meta-data/placement/availability-zone') - .reply(500); + .reply(400); const client = new AwsClient(awsOptions); await assert.rejects(client.retrieveSubjectToken(), { - status: 500, + status: 400, }); scope.done(); }); @@ -982,12 +982,12 @@ describe('AwsClient', () => { const scope = nock(metadataBaseUrl) .get('/latest/meta-data/placement/availability-zone') - .reply(500); + .reply(400); const client = new AwsClient(awsOptions); await assert.rejects(client.getAccessToken(), { - status: 500, + status: 400, }); scope.done(); }); diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index b808be72..067155b3 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -17,7 +17,7 @@ import {describe, it, beforeEach, afterEach} from 'mocha'; import * as nock from 'nock'; import * as sinon from 'sinon'; -import {GaxiosError, GaxiosOptions, GaxiosPromise} from 'gaxios'; +import {GaxiosError, GaxiosPromise} from 'gaxios'; import {Credentials} from '../src/auth/credentials'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; import { diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index c97e4b03..5d0e7920 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -227,7 +227,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { scope.done(); }); - it('should handle refresh timeout', async () => { + it('should handle and retry on timeout', async () => { + // we need timers/`setTimeout` for this test + clock.restore(); + const expectedRequest = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: 'refreshToken', @@ -239,13 +242,19 @@ describe('ExternalAccountAuthorizedUserClient', () => { }, }) .post(REFRESH_PATH, expectedRequest.toString()) - .replyWithError({code: 'ETIMEDOUT'}); + .replyWithError({code: 'ETIMEDOUT'}) + .post(REFRESH_PATH, expectedRequest.toString()) + .reply(200, successfulRefreshResponse); const client = new ExternalAccountAuthorizedUserClient( externalAccountAuthorizedUserCredentialOptions ); - await assert.rejects(client.getAccessToken(), { - code: 'ETIMEDOUT', + + const actualResponse = await client.getAccessToken(); + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: successfulRefreshResponse.access_token, }); scope.done(); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 13b4695e..1d1ee013 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -200,7 +200,9 @@ describe('jwt', () => { const got = await jwt.getRequestHeaders(testUri); assert.notStrictEqual(null, got, 'the creds should be present'); const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); - assert.deepStrictEqual({alg: 'RS256', typ: 'JWT'}, decoded.header); + assert(decoded); + assert.strictEqual(decoded.header.alg, 'RS256'); + assert.strictEqual(decoded.header.typ, 'JWT'); const payload = decoded.payload; assert.strictEqual(email, payload.iss); assert.strictEqual(email, payload.sub); @@ -221,10 +223,12 @@ describe('jwt', () => { const got = await jwt.getRequestHeaders(testUri); assert.notStrictEqual(null, got, 'the creds should be present'); const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); - assert.deepStrictEqual( - {alg: 'RS256', typ: 'JWT', kid: '101'}, - decoded.header - ); + assert(decoded); + assert.deepStrictEqual(decoded.header, { + alg: 'RS256', + typ: 'JWT', + kid: '101', + }); }); it('should accept additionalClaims', async () => { @@ -244,6 +248,7 @@ describe('jwt', () => { const got = await jwt.getRequestHeaders(testUri); assert.notStrictEqual(null, got, 'the creds should be present'); const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); + assert(decoded); const payload = decoded.payload; assert.strictEqual(testDefault, payload.aud); assert.strictEqual(someClaim, payload.someClaim); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index 1434f256..d441e3b0 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -49,7 +49,8 @@ describe('jwtaccess', () => { const headers = client.getRequestHeaders(testUri); assert.notStrictEqual(null, headers, 'an creds object should be present'); const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); - assert.deepStrictEqual({alg: 'RS256', typ: 'JWT'}, decoded.header); + assert(decoded); + assert.deepStrictEqual(decoded.header, {alg: 'RS256', typ: 'JWT'}); const payload = decoded.payload; assert.strictEqual(email, payload.iss); assert.strictEqual(email, payload.sub); @@ -60,6 +61,7 @@ describe('jwtaccess', () => { const client = new JWTAccess(email, keys.private); const headers = client.getRequestHeaders(testUri, undefined, 'myfakescope'); const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + assert(decoded); const payload = decoded.payload; assert.strictEqual('myfakescope', payload.scope); }); @@ -68,6 +70,7 @@ describe('jwtaccess', () => { const client = new JWTAccess(email, keys.private); const headers = client.getRequestHeaders(testUri); const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + assert(decoded); const payload = decoded.payload; assert.strictEqual(testUri, payload.aud); }); @@ -76,10 +79,12 @@ describe('jwtaccess', () => { const client = new JWTAccess(email, keys.private, '101'); const headers = client.getRequestHeaders(testUri); const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); - assert.deepStrictEqual( - {alg: 'RS256', typ: 'JWT', kid: '101'}, - decoded.header - ); + assert(decoded); + assert.deepStrictEqual(decoded.header, { + alg: 'RS256', + typ: 'JWT', + kid: '101', + }); }); it('getRequestHeaders should not allow overriding with additionalClaims', () => { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 6156b8aa..b836656f 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -992,7 +992,7 @@ describe('oauth2', () => { reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, }) .post('/token') - .reply(500) + .reply(400) .post('/token') .reply(200, {access_token: 'abc123', expires_in: 100000}), nock('http://example.com').get('/').reply(200), @@ -1020,7 +1020,7 @@ describe('oauth2', () => { reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, }) .post('/token') - .reply(500, reAuthErrorBody), + .reply(400, reAuthErrorBody), ]; client.credentials = {refresh_token: 'refresh-token-placeholder'}; diff --git a/test/test.stscredentials.ts b/test/test.stscredentials.ts index 72e17119..4aafd20d 100644 --- a/test/test.stscredentials.ts +++ b/test/test.stscredentials.ts @@ -216,26 +216,31 @@ describe('StsCredentials', () => { scope.done(); }); - it('should handle request timeout', async () => { + it('should handle and retry on timeout', async () => { const scope = nock(baseUrl) .post(path, qs.stringify(expectedRequest), { reqheaders: { 'content-type': 'application/x-www-form-urlencoded', }, }) - .replyWithError({code: 'ETIMEDOUT'}); + .replyWithError({code: 'ETIMEDOUT'}) + .post(path, qs.stringify(expectedRequest), { + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }) + .reply(200, stsSuccessfulResponse); const stsCredentials = new StsCredentials(tokenExchangeEndpoint); - await assert.rejects( - stsCredentials.exchangeToken( - stsCredentialsOptions, - additionalHeaders, - options - ), - { - code: 'ETIMEDOUT', - } + const resp = await stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options ); + + assertGaxiosResponsePresent(resp); + delete resp.res; + assert.deepStrictEqual(resp, stsSuccessfulResponse); scope.done(); }); }); From 5058726e2234a2da4edd31f0898465798943ebe6 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 12 Apr 2024 14:09:21 -0700 Subject: [PATCH 517/662] feat: Improve `gaxios` exposure (#1794) * feat: Improve `gaxios` exposure * docs: clarify --- src/auth/authclient.ts | 20 ++++++++++++++++++++ src/index.ts | 3 +++ 2 files changed, 23 insertions(+) diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 8e004d63..69301db8 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -207,6 +207,26 @@ export abstract class AuthClient this.forceRefreshOnFailure = opts.forceRefreshOnFailure ?? false; } + /** + * Return the {@link Gaxios `Gaxios`} instance from the {@link AuthClient.transporter}. + * + * @expiremental + */ + get gaxios(): Gaxios | null { + if (this.transporter instanceof Gaxios) { + return this.transporter; + } else if (this.transporter instanceof DefaultTransporter) { + return this.transporter.instance; + } else if ( + 'instance' in this.transporter && + this.transporter.instance instanceof Gaxios + ) { + return this.transporter.instance; + } + + return null; + } + /** * Provides an alternative Gaxios request implementation with auth credentials */ diff --git a/src/index.ts b/src/index.ts index 77ad950a..1ab32948 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,10 @@ // limitations under the License. import {GoogleAuth} from './auth/googleauth'; +// Export common deps to ensure types/instances are the exact match. Useful +// for consistently configuring the library across versions. export * as gcpMetadata from 'gcp-metadata'; +export * as gaxios from 'gaxios'; export {AuthClient, DEFAULT_UNIVERSE} from './auth/authclient'; export {Compute, ComputeOptions} from './auth/computeclient'; From b2eae07d754ae60565217f58fd5f45bd82385c60 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:03:52 -0700 Subject: [PATCH 518/662] chore(main): release 9.8.0 (#1777) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22516d27..42522f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.8.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.7.0...v9.8.0) (2024-04-12) + + +### Features + +* Enable Retries For Auth Requests ([#1791](https://github.com/googleapis/google-auth-library-nodejs/issues/1791)) ([9b69a31](https://github.com/googleapis/google-auth-library-nodejs/commit/9b69a3119c2d0dfe12d41a5f77658d35a2c92d74)) +* Improve `gaxios` exposure ([#1794](https://github.com/googleapis/google-auth-library-nodejs/issues/1794)) ([5058726](https://github.com/googleapis/google-auth-library-nodejs/commit/5058726e2234a2da4edd31f0898465798943ebe6)) + + +### Bug Fixes + +* **deps:** Update dependency open to v10 ([#1782](https://github.com/googleapis/google-auth-library-nodejs/issues/1782)) ([16e5cae](https://github.com/googleapis/google-auth-library-nodejs/commit/16e5cae1d56d5c3dd6cc3bdca5ecdfb534eaf529)) +* **deps:** Update dependency opn to v6 ([#1775](https://github.com/googleapis/google-auth-library-nodejs/issues/1775)) ([fc8dfe9](https://github.com/googleapis/google-auth-library-nodejs/commit/fc8dfe9d373e30dd1bd06eb8cbb8b52e735b4d83)) + ## [9.7.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.6.3...v9.7.0) (2024-03-12) diff --git a/package.json b/package.json index ce217c30..4eb160a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.7.0", + "version": "9.8.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 540f12c9..a2926e4f 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^15.0.0", - "google-auth-library": "^9.7.0", + "google-auth-library": "^9.8.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From c680b5ddfa526d414ad1250bb6f5af69c498b909 Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:06:12 -0700 Subject: [PATCH 519/662] feat: adds suppliers for custom subject token and AWS credentials (#1795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: refactor AWS and identity pool clients to use suppliers (#1776) * feat: refactor aws and identity pool credentials to use suppliers * Apply suggestions from code review Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Daniel Bankhead * updating suppliers to use options objects * updating docs * moved transporter to context object and deprecated consts * fix imports --------- Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> Co-authored-by: Daniel Bankhead * feat: adds support for creating AWS and Identity Pool credentials with custom suppliers. (#1783) * feat: adds support for users to build credentials with custom suppliers Also adds default values to make it easier to instantiate credentials in code. * Apply suggestions from code review Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * responding to review comments --------- Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * docs: adding documentation for programmatic auth feature (#1790) * docs: adding documentation for programmatic auth feature * fix typos * Apply suggestions from code review Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> Co-authored-by: Daniel Bankhead * add audience placeholder --------- Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> Co-authored-by: Daniel Bankhead * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix lint --------- Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> Co-authored-by: Daniel Bankhead Co-authored-by: Owl Bot --- .readme-partials.yaml | 126 +++++++ README.md | 126 +++++++ src/auth/awsclient.ts | 346 +++++++----------- src/auth/baseexternalclient.ts | 98 ++++- .../defaultawssecuritycredentialssupplier.ts | 272 ++++++++++++++ .../externalAccountAuthorizedUserClient.ts | 11 +- src/auth/filesubjecttokensupplier.ts | 114 ++++++ src/auth/identitypoolclient.ts | 269 ++++++-------- src/auth/urlsubjecttokensupplier.ts | 108 ++++++ test/test.awsclient.ts | 273 +++++++++++++- test/test.baseexternalclient.ts | 64 ++++ ...est.externalaccountauthorizeduserclient.ts | 46 +++ test/test.identitypoolclient.ts | 272 +++++++++++++- 13 files changed, 1744 insertions(+), 381 deletions(-) create mode 100644 src/auth/defaultawssecuritycredentialssupplier.ts create mode 100644 src/auth/filesubjecttokensupplier.ts create mode 100644 src/auth/urlsubjecttokensupplier.ts diff --git a/.readme-partials.yaml b/.readme-partials.yaml index d4c55051..cce6a02f 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -356,6 +356,55 @@ body: |- You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from AWS. + ### Accessing resources from AWS using a custom AWS security credentials supplier. + + In order to access Google Cloud resources from Amazon Web Services (AWS), the following requirements are needed: + - A workload identity pool needs to be created. + - AWS needs to be added as an identity provider in the workload identity pool (The Google [organization policy](https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict) needs to allow federation from AWS). + - Permission to impersonate a service account needs to be granted to the external identity. + + Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-resources-aws) on how to configure workload identity federation from AWS. + + If you want to use AWS security credentials that cannot be retrieved using methods supported natively by this library, + a custom AwsSecurityCredentialsSupplier implementation may be specified when creating an AWS client. The supplier must + return valid, unexpired AWS security credentials when called by the GCP credential. + + Note that the client does not cache the returned AWS security credentials, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. + + ```ts + class AwsSupplier implements AwsSecurityCredentialsSupplier { + async getAwsRegion(context: ExternalAccountSupplierContext): Promise { + // Return the current AWS region, i.e. 'us-east-2'. + } + + async getAwsSecurityCredentials( + context: ExternalAccountSupplierContext + ): Promise { + const audience = context.audience; + // Return valid AWS security credentials for the requested audience. + } + } + + const clientOptions = { + audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type. + aws_security_credentials_supplier: new AwsSupplier() // Set the custom supplier. + } + + const client = new AwsClient(clientOptions); + ``` + + Where the [audience](https://cloud.google.com/iam/docs/best-practices-for-using-workload-identity-federation#provider-audience) is: `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` + + Where the following variables need to be substituted: + + * `$PROJECT_NUMBER`: The Google Cloud project number. + * `$WORKLOAD_POOL_ID`: The workload pool ID. + * `$PROVIDER_ID`: The provider ID. + + + The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/sdk/gcloud/reference/iam/workload-identity-pools/create-cred-config). + ### Access resources from Microsoft Azure In order to access Google Cloud resources from Microsoft Azure, the following requirements are needed: @@ -464,6 +513,44 @@ body: |- - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. + ### Accessing resources from an OIDC or SAML2.0 identity provider using a custom supplier + + If you want to use OIDC or SAML2.0 that cannot be retrieved using methods supported natively by this library, + a custom SubjectTokenSupplier implementation may be specified when creating an identity pool client. The supplier must + return a valid, unexpired subject token when called by the GCP credential. + + Note that the client does not cache the returned subject token, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. + + ```ts + class CustomSupplier implements SubjectTokenSupplier { + async getSubjectToken( + context: ExternalAccountSupplierContext + ): Promise { + const audience = context.audience; + const subjectTokenType = context.subjectTokenType; + // Return a valid subject token for the requested audience and subject token type. + } + } + + const clientOptions = { + audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', // Set the subject token type. + subject_token_supplier: new CustomSupplier() // Set the custom supplier. + } + + const client = new CustomSupplier(clientOptions); + ``` + + Where the [audience](https://cloud.google.com/iam/docs/best-practices-for-using-workload-identity-federation#provider-audience) is: `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` + + Where the following variables need to be substituted: + + * `$PROJECT_NUMBER`: The Google Cloud project number. + * `$WORKLOAD_POOL_ID`: The workload pool ID. + * `$PROVIDER_ID`: The provider ID. + + The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/sdk/gcloud/reference/iam/workload-identity-pools/create-cred-config). + #### Using External Account Authorized User workforce credentials [External account authorized user credentials](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#browser-based-sign-in) allow you to sign in with a web browser to an external identity provider account via the @@ -842,6 +929,45 @@ body: |- You can now [use the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC or SAML provider. + ### Accessing resources from an OIDC or SAML2.0 identity provider using a custom supplier + + If you want to use OIDC or SAML2.0 that cannot be retrieved using methods supported natively by this library, + a custom SubjectTokenSupplier implementation may be specified when creating an identity pool client. The supplier must + return a valid, unexpired subject token when called by the GCP credential. + + Note that the client does not cache the returned subject token, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. + + ```ts + class CustomSupplier implements SubjectTokenSupplier { + async getSubjectToken( + context: ExternalAccountSupplierContext + ): Promise { + const audience = context.audience; + const subjectTokenType = context.subjectTokenType; + // Return a valid subject token for the requested audience and subject token type. + } + } + + const clientOptions = { + audience: '//iam.googleapis.com/locations/global/workforcePools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', // Set the subject token type. + subject_token_supplier: new CustomSupplier() // Set the custom supplier. + } + + const client = new CustomSupplier(clientOptions); + ``` + + Where the audience is: `//iam.googleapis.com/locations/global/workforcePools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` + + Where the following variables need to be substituted: + + * `WORKFORCE_POOL_ID`: The worforce pool ID. + * `$PROVIDER_ID`: The provider ID. + + and the workforce pool user project is the project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + + The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#use_configuration_files_for_sign-in). + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. diff --git a/README.md b/README.md index 884cbbc9..55d86876 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,55 @@ The gcloud create-cred-config command will be updated to support this soon. You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from AWS. +### Accessing resources from AWS using a custom AWS security credentials supplier. + +In order to access Google Cloud resources from Amazon Web Services (AWS), the following requirements are needed: +- A workload identity pool needs to be created. +- AWS needs to be added as an identity provider in the workload identity pool (The Google [organization policy](https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict) needs to allow federation from AWS). +- Permission to impersonate a service account needs to be granted to the external identity. + +Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-resources-aws) on how to configure workload identity federation from AWS. + +If you want to use AWS security credentials that cannot be retrieved using methods supported natively by this library, +a custom AwsSecurityCredentialsSupplier implementation may be specified when creating an AWS client. The supplier must +return valid, unexpired AWS security credentials when called by the GCP credential. + +Note that the client does not cache the returned AWS security credentials, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. + +```ts +class AwsSupplier implements AwsSecurityCredentialsSupplier { + async getAwsRegion(context: ExternalAccountSupplierContext): Promise { + // Return the current AWS region, i.e. 'us-east-2'. + } + + async getAwsSecurityCredentials( + context: ExternalAccountSupplierContext + ): Promise { + const audience = context.audience; + // Return valid AWS security credentials for the requested audience. + } +} + +const clientOptions = { + audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type. + aws_security_credentials_supplier: new AwsSupplier() // Set the custom supplier. +} + +const client = new AwsClient(clientOptions); +``` + +Where the [audience](https://cloud.google.com/iam/docs/best-practices-for-using-workload-identity-federation#provider-audience) is: `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` + +Where the following variables need to be substituted: + +* `$PROJECT_NUMBER`: The Google Cloud project number. +* `$WORKLOAD_POOL_ID`: The workload pool ID. +* `$PROVIDER_ID`: The provider ID. + + +The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/sdk/gcloud/reference/iam/workload-identity-pools/create-cred-config). + ### Access resources from Microsoft Azure In order to access Google Cloud resources from Microsoft Azure, the following requirements are needed: @@ -508,6 +557,44 @@ Where the following variables need to be substituted: - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. +### Accessing resources from an OIDC or SAML2.0 identity provider using a custom supplier + +If you want to use OIDC or SAML2.0 that cannot be retrieved using methods supported natively by this library, +a custom SubjectTokenSupplier implementation may be specified when creating an identity pool client. The supplier must +return a valid, unexpired subject token when called by the GCP credential. + +Note that the client does not cache the returned subject token, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. + +```ts +class CustomSupplier implements SubjectTokenSupplier { + async getSubjectToken( + context: ExternalAccountSupplierContext + ): Promise { + const audience = context.audience; + const subjectTokenType = context.subjectTokenType; + // Return a valid subject token for the requested audience and subject token type. + } +} + +const clientOptions = { + audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', // Set the subject token type. + subject_token_supplier: new CustomSupplier() // Set the custom supplier. +} + +const client = new CustomSupplier(clientOptions); +``` + +Where the [audience](https://cloud.google.com/iam/docs/best-practices-for-using-workload-identity-federation#provider-audience) is: `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` + +Where the following variables need to be substituted: + +* `$PROJECT_NUMBER`: The Google Cloud project number. +* `$WORKLOAD_POOL_ID`: The workload pool ID. +* `$PROVIDER_ID`: The provider ID. + +The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/sdk/gcloud/reference/iam/workload-identity-pools/create-cred-config). + #### Using External Account Authorized User workforce credentials [External account authorized user credentials](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#browser-based-sign-in) allow you to sign in with a web browser to an external identity provider account via the @@ -886,6 +973,45 @@ credentials unless they do not meet your specific requirements. You can now [use the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC or SAML provider. +### Accessing resources from an OIDC or SAML2.0 identity provider using a custom supplier + +If you want to use OIDC or SAML2.0 that cannot be retrieved using methods supported natively by this library, +a custom SubjectTokenSupplier implementation may be specified when creating an identity pool client. The supplier must +return a valid, unexpired subject token when called by the GCP credential. + +Note that the client does not cache the returned subject token, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. + +```ts +class CustomSupplier implements SubjectTokenSupplier { + async getSubjectToken( + context: ExternalAccountSupplierContext + ): Promise { + const audience = context.audience; + const subjectTokenType = context.subjectTokenType; + // Return a valid subject token for the requested audience and subject token type. + } +} + +const clientOptions = { + audience: '//iam.googleapis.com/locations/global/workforcePools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', // Set the subject token type. + subject_token_supplier: new CustomSupplier() // Set the custom supplier. +} + +const client = new CustomSupplier(clientOptions); +``` + +Where the audience is: `//iam.googleapis.com/locations/global/workforcePools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` + +Where the following variables need to be substituted: + +* `WORKFORCE_POOL_ID`: The worforce pool ID. +* `$PROVIDER_ID`: The provider ID. + +and the workforce pool user project is the project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). + +The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#use_configuration_files_for_sign-in). + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index f9f899d9..d1c3155f 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -12,51 +12,92 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {GaxiosOptions} from 'gaxios'; - import {AwsRequestSigner, AwsSecurityCredentials} from './awsrequestsigner'; import { BaseExternalAccountClient, BaseExternalAccountClientOptions, + ExternalAccountSupplierContext, } from './baseexternalclient'; -import {Headers} from './oauth2client'; import {AuthClientOptions} from './authclient'; +import {DefaultAwsSecurityCredentialsSupplier} from './defaultawssecuritycredentialssupplier'; +import {originalOrCamelOptions, SnakeToCamelObject} from '../util'; /** * AWS credentials JSON interface. This is used for AWS workloads. */ export interface AwsClientOptions extends BaseExternalAccountClientOptions { - credential_source: { + /** + * Object containing options to retrieve AWS security credentials. A valid credential + * source or a aws security credentials supplier should be specified. + */ + credential_source?: { + /** + * AWS environment ID. Currently only 'AWS1' is supported. + */ environment_id: string; - // Region can also be determined from the AWS_REGION or AWS_DEFAULT_REGION - // environment variables. + /** + * The EC2 metadata URL to retrieve the current AWS region from. If this is + * not provided, the region should be present in the AWS_REGION or AWS_DEFAULT_REGION + * environment variables. + */ region_url?: string; - // The url field is used to determine the AWS security credentials. - // This is optional since these credentials can be retrieved from the - // AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN - // environment variables. + /** + * The EC2 metadata URL to retrieve AWS security credentials. If this is not provided, + * the credentials should be present in the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, + * and AWS_SESSION_TOKEN environment variables. + */ url?: string; + /** + * The regional GetCallerIdentity action URL, used to determine the account + * ID and its roles. + */ regional_cred_verification_url: string; - // The imdsv2 session token url is used to fetch session token from AWS - // which is later sent through headers for metadata requests. If the - // field is missing, then session token won't be fetched and sent with - // the metadata requests. - // The session token is required for IMDSv2 but optional for IMDSv1 + /** + * The imdsv2 session token url is used to fetch session token from AWS + * which is later sent through headers for metadata requests. If the + * field is missing, then session token won't be fetched and sent with + * the metadata requests. + * The session token is required for IMDSv2 but optional for IMDSv1 + */ imdsv2_session_token_url?: string; }; + /** + * The AWS security credentials supplier to call to retrieve the AWS region + * and AWS security credentials. Either this or a valid credential source + * must be specified. + */ + aws_security_credentials_supplier?: AwsSecurityCredentialsSupplier; } /** - * Interface defining the AWS security-credentials endpoint response. + * Supplier interface for AWS security credentials. This can be implemented to + * return an AWS region and AWS security credentials. These credentials can + * then be exchanged for a GCP token by an {@link AwsClient}. */ -interface AwsSecurityCredentialsResponse { - Code: string; - LastUpdated: string; - Type: string; - AccessKeyId: string; - SecretAccessKey: string; - Token: string; - Expiration: string; +export interface AwsSecurityCredentialsSupplier { + /** + * Gets the active AWS region. + * @param context {@link ExternalAccountSupplierContext} from the calling + * {@link AwsClient}, contains the requested audience and subject token type + * for the external account identity as well as the transport from the + * calling client to use for requests. + * @return A promise that resolves with the AWS region string. + */ + getAwsRegion: (context: ExternalAccountSupplierContext) => Promise; + + /** + * Gets valid AWS security credentials for the requested external account + * identity. Note that these are not cached by the calling {@link AwsClient}, + * so caching should be including in the implementation. + * @param context {@link ExternalAccountSupplierContext} from the calling + * {@link AwsClient}, contains the requested audience and subject token type + * for the external account identity as well as the transport from the + * calling client to use for requests. + * @return A promise that resolves with the requested {@link AwsSecurityCredentials}. + */ + getAwsSecurityCredentials: ( + context: ExternalAccountSupplierContext + ) => Promise; } /** @@ -65,15 +106,22 @@ interface AwsSecurityCredentialsResponse { * GCP access token. */ export class AwsClient extends BaseExternalAccountClient { - private readonly environmentId: string; - private readonly regionUrl?: string; - private readonly securityCredentialsUrl?: string; + private readonly environmentId?: string; + private readonly awsSecurityCredentialsSupplier: AwsSecurityCredentialsSupplier; private readonly regionalCredVerificationUrl: string; - private readonly imdsV2SessionTokenUrl?: string; private awsRequestSigner: AwsRequestSigner | null; private region: string; + static #DEFAULT_AWS_REGIONAL_CREDENTIAL_VERIFICATION_URL = + 'https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15'; + + /** + * @deprecated AWS client no validates the EC2 metadata address. + **/ static AWS_EC2_METADATA_IPV4_ADDRESS = '169.254.169.254'; + /** + * @deprecated AWS client no validates the EC2 metadata address. + **/ static AWS_EC2_METADATA_IPV6_ADDRESS = 'fd00:ec2::254'; /** @@ -88,27 +136,61 @@ export class AwsClient extends BaseExternalAccountClient { * on 401/403 API request errors. */ constructor( - options: AwsClientOptions, + options: AwsClientOptions | SnakeToCamelObject, additionalOptions?: AuthClientOptions ) { super(options, additionalOptions); - this.environmentId = options.credential_source.environment_id; - // This is only required if the AWS region is not available in the - // AWS_REGION or AWS_DEFAULT_REGION environment variables. - this.regionUrl = options.credential_source.region_url; - // This is only required if AWS security credentials are not available in - // environment variables. - this.securityCredentialsUrl = options.credential_source.url; - this.regionalCredVerificationUrl = - options.credential_source.regional_cred_verification_url; - this.imdsV2SessionTokenUrl = - options.credential_source.imdsv2_session_token_url; + const opts = originalOrCamelOptions(options as AwsClientOptions); + const credentialSource = opts.get('credential_source'); + const awsSecurityCredentialsSupplier = opts.get( + 'aws_security_credentials_supplier' + ); + // Validate credential sourcing configuration. + if (!credentialSource && !awsSecurityCredentialsSupplier) { + throw new Error( + 'A credential source or AWS security credentials supplier must be specified.' + ); + } + if (credentialSource && awsSecurityCredentialsSupplier) { + throw new Error( + 'Only one of credential source or AWS security credentials supplier can be specified.' + ); + } + + if (awsSecurityCredentialsSupplier) { + this.awsSecurityCredentialsSupplier = awsSecurityCredentialsSupplier; + this.regionalCredVerificationUrl = + AwsClient.#DEFAULT_AWS_REGIONAL_CREDENTIAL_VERIFICATION_URL; + this.credentialSourceType = 'programmatic'; + } else { + const credentialSourceOpts = originalOrCamelOptions(credentialSource); + this.environmentId = credentialSourceOpts.get('environment_id'); + // This is only required if the AWS region is not available in the + // AWS_REGION or AWS_DEFAULT_REGION environment variables. + const regionUrl = credentialSourceOpts.get('region_url'); + // This is only required if AWS security credentials are not available in + // environment variables. + const securityCredentialsUrl = credentialSourceOpts.get('url'); + const imdsV2SessionTokenUrl = credentialSourceOpts.get( + 'imdsv2_session_token_url' + ); + this.awsSecurityCredentialsSupplier = + new DefaultAwsSecurityCredentialsSupplier({ + regionUrl: regionUrl, + securityCredentialsUrl: securityCredentialsUrl, + imdsV2SessionTokenUrl: imdsV2SessionTokenUrl, + }); + + this.regionalCredVerificationUrl = credentialSourceOpts.get( + 'regional_cred_verification_url' + ); + this.credentialSourceType = 'aws'; + + // Data validators. + this.validateEnvironmentId(); + } this.awsRequestSigner = null; this.region = ''; - this.credentialSourceType = 'aws'; - - // Data validators. - this.validateEnvironmentId(); } private validateEnvironmentId() { @@ -124,68 +206,22 @@ export class AwsClient extends BaseExternalAccountClient { /** * Triggered when an external subject token is needed to be exchanged for a - * GCP access token via GCP STS endpoint. - * This uses the `options.credential_source` object to figure out how - * to retrieve the token using the current environment. In this case, - * this uses a serialized AWS signed request to the STS GetCallerIdentity - * endpoint. - * The logic is summarized as: - * 1. If imdsv2_session_token_url is provided in the credential source, then - * fetch the aws session token and include it in the headers of the - * metadata requests. This is a requirement for IDMSv2 but optional - * for IDMSv1. - * 2. Retrieve AWS region from availability-zone. - * 3a. Check AWS credentials in environment variables. If not found, get - * from security-credentials endpoint. - * 3b. Get AWS credentials from security-credentials endpoint. In order - * to retrieve this, the AWS role needs to be determined by calling - * security-credentials endpoint without any argument. Then the - * credentials can be retrieved via: security-credentials/role_name - * 4. Generate the signed request to AWS STS GetCallerIdentity action. - * 5. Inject x-goog-cloud-target-resource into header and serialize the - * signed request. This will be the subject-token to pass to GCP STS. + * GCP access token via GCP STS endpoint. This will call the + * {@link AwsSecurityCredentialsSupplier} to retrieve an AWS region and AWS + * Security Credentials, then use them to create a signed AWS STS request that + * can be exchanged for a GCP access token. * @return A promise that resolves with the external subject token. */ async retrieveSubjectToken(): Promise { // Initialize AWS request signer if not already initialized. if (!this.awsRequestSigner) { - const metadataHeaders: Headers = {}; - // Only retrieve the IMDSv2 session token if both the security credentials and region are - // not retrievable through the environment. - // The credential config contains all the URLs by default but clients may be running this - // where the metadata server is not available and returning the credentials through the environment. - // Removing this check may break them. - if (!this.regionFromEnv && this.imdsV2SessionTokenUrl) { - metadataHeaders['x-aws-ec2-metadata-token'] = - await this.getImdsV2SessionToken(); - } - - this.region = await this.getAwsRegion(metadataHeaders); + this.region = await this.awsSecurityCredentialsSupplier.getAwsRegion( + this.supplierContext + ); this.awsRequestSigner = new AwsRequestSigner(async () => { - // Check environment variables for permanent credentials first. - // https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html - if (this.securityCredentialsFromEnv) { - return this.securityCredentialsFromEnv; - } - if (this.imdsV2SessionTokenUrl) { - metadataHeaders['x-aws-ec2-metadata-token'] = - await this.getImdsV2SessionToken(); - } - // Since the role on a VM can change, we don't need to cache it. - const roleName = await this.getAwsRoleName(metadataHeaders); - // Temporary credentials typically last for several hours. - // Expiration is returned in response. - // Consider future optimization of this logic to cache AWS tokens - // until their natural expiration. - const awsCreds = await this.getAwsSecurityCredentials( - roleName, - metadataHeaders + return this.awsSecurityCredentialsSupplier.getAwsSecurityCredentials( + this.supplierContext ); - return { - accessKeyId: awsCreds.AccessKeyId, - secretAccessKey: awsCreds.SecretAccessKey, - token: awsCreds.Token, - }; }, this.region); } @@ -235,116 +271,4 @@ export class AwsClient extends BaseExternalAccountClient { }) ); } - - /** - * @return A promise that resolves with the IMDSv2 Session Token. - */ - private async getImdsV2SessionToken(): Promise { - const opts: GaxiosOptions = { - ...AwsClient.RETRY_CONFIG, - url: this.imdsV2SessionTokenUrl, - method: 'PUT', - responseType: 'text', - headers: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, - }; - const response = await this.transporter.request(opts); - return response.data; - } - - /** - * @param headers The headers to be used in the metadata request. - * @return A promise that resolves with the current AWS region. - */ - private async getAwsRegion(headers: Headers): Promise { - // Priority order for region determination: - // AWS_REGION > AWS_DEFAULT_REGION > metadata server. - if (this.regionFromEnv) { - return this.regionFromEnv; - } - if (!this.regionUrl) { - throw new Error( - 'Unable to determine AWS region due to missing ' + - '"options.credential_source.region_url"' - ); - } - const opts: GaxiosOptions = { - ...AwsClient.RETRY_CONFIG, - url: this.regionUrl, - method: 'GET', - responseType: 'text', - headers: headers, - }; - const response = await this.transporter.request(opts); - // Remove last character. For example, if us-east-2b is returned, - // the region would be us-east-2. - return response.data.substr(0, response.data.length - 1); - } - - /** - * @param headers The headers to be used in the metadata request. - * @return A promise that resolves with the assigned role to the current - * AWS VM. This is needed for calling the security-credentials endpoint. - */ - private async getAwsRoleName(headers: Headers): Promise { - if (!this.securityCredentialsUrl) { - throw new Error( - 'Unable to determine AWS role name due to missing ' + - '"options.credential_source.url"' - ); - } - const opts: GaxiosOptions = { - ...AwsClient.RETRY_CONFIG, - url: this.securityCredentialsUrl, - method: 'GET', - responseType: 'text', - headers: headers, - }; - const response = await this.transporter.request(opts); - return response.data; - } - - /** - * Retrieves the temporary AWS credentials by calling the security-credentials - * endpoint as specified in the `credential_source` object. - * @param roleName The role attached to the current VM. - * @param headers The headers to be used in the metadata request. - * @return A promise that resolves with the temporary AWS credentials - * needed for creating the GetCallerIdentity signed request. - */ - private async getAwsSecurityCredentials( - roleName: string, - headers: Headers - ): Promise { - const response = - await this.transporter.request({ - ...AwsClient.RETRY_CONFIG, - url: `${this.securityCredentialsUrl}/${roleName}`, - responseType: 'json', - headers: headers, - }); - return response.data; - } - - private get regionFromEnv(): string | null { - // The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. - // Only one is required. - return ( - process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'] || null - ); - } - - private get securityCredentialsFromEnv(): AwsSecurityCredentials | null { - // Both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required. - if ( - process.env['AWS_ACCESS_KEY_ID'] && - process.env['AWS_SECRET_ACCESS_KEY'] - ) { - return { - accessKeyId: process.env['AWS_ACCESS_KEY_ID'], - secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'], - token: process.env['AWS_SESSION_TOKEN'], - }; - } - return null; - } } diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 94789556..3513e7e2 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -13,6 +13,7 @@ // limitations under the License. import { + Gaxios, GaxiosError, GaxiosOptions, GaxiosPromise, @@ -22,7 +23,7 @@ import * as stream from 'stream'; import {Credentials} from './credentials'; import {AuthClient, AuthClientOptions} from './authclient'; -import {BodyResponseCallback} from '../transporters'; +import {BodyResponseCallback, Transporter} from '../transporters'; import {GetAccessTokenResponse, Headers} from './oauth2client'; import * as sts from './stscredentials'; import {ClientAuthentication} from './oauth2common'; @@ -63,6 +64,7 @@ export const CLOUD_RESOURCE_MANAGER = /** The workforce audience pattern. */ const WORKFORCE_AUDIENCE_PATTERN = '//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; +const DEFAULT_TOKEN_URL = 'https://sts.{universeDomain}/v1/token'; // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); @@ -72,9 +74,47 @@ const pkg = require('../../../package.json'); */ export {DEFAULT_UNIVERSE} from './authclient'; +/** + * Shared options used to build {@link ExternalAccountClient} and + * {@link ExternalAccountAuthorizedUserClient}. + */ export interface SharedExternalAccountClientOptions extends AuthClientOptions { + /** + * The Security Token Service audience, which is usually the fully specified + * resource name of the workload or workforce pool provider. + */ audience: string; - token_url: string; + /** + * The Security Token Service token URL used to exchange the third party token + * for a GCP access token. If not provided, will default to + * 'https://sts.googleapis.com/v1/token' + */ + token_url?: string; +} + +/** + * Interface containing context about the requested external identity. This is + * passed on all requests from external account clients to external identity suppliers. + */ +export interface ExternalAccountSupplierContext { + /** + * The requested external account audience. For example: + * * "//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID" + * * "//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID" + */ + audience: string; + /** + * The requested subject token type. Expected values include: + * * "urn:ietf:params:oauth:token-type:jwt" + * * "urn:ietf:params:aws:token-type:aws4_request" + * * "urn:ietf:params:oauth:token-type:saml2" + * * "urn:ietf:params:oauth:token-type:id_token" + */ + subjectTokenType: string; + /** The {@link Gaxios} or {@link Transporter} instance from + * the calling external account to use for requests. + */ + transporter: Transporter | Gaxios; } /** @@ -82,16 +122,55 @@ export interface SharedExternalAccountClientOptions extends AuthClientOptions { */ export interface BaseExternalAccountClientOptions extends SharedExternalAccountClientOptions { - type: string; + /** + * Credential type, should always be 'external_account'. + */ + type?: string; + /** + * The Security Token Service subject token type based on the OAuth 2.0 + * token exchange spec. Expected values include: + * * 'urn:ietf:params:oauth:token-type:jwt' + * * 'urn:ietf:params:aws:token-type:aws4_request' + * * 'urn:ietf:params:oauth:token-type:saml2' + * * 'urn:ietf:params:oauth:token-type:id_token' + */ subject_token_type: string; + /** + * The URL for the service account impersonation request. This URL is required + * for some APIs. If this URL is not available, the access token from the + * Security Token Service is used directly. + */ service_account_impersonation_url?: string; + /** + * Object containing additional options for service account impersonation. + */ service_account_impersonation?: { + /** + * The desired lifetime of the impersonated service account access token. + * If not provided, the default lifetime will be 3600 seconds. + */ token_lifetime_seconds?: number; }; + /** + * The endpoint used to retrieve account related information. + */ token_info_url?: string; + /** + * Client ID of the service account from the console. + */ client_id?: string; + /** + * Client secret of the service account from the console. + */ client_secret?: string; + /** + * The workforce pool user project. Required when using a workforce identity + * pool. + */ workforce_pool_user_project?: string; + /** + * The scopes to request during the authorization grant. + */ scopes?: string[]; /** * @example @@ -167,6 +246,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { * ``` */ protected cloudResourceManagerURL: URL | string; + protected supplierContext: ExternalAccountSupplierContext; /** * Instantiate a BaseExternalAccountClient instance using the provided JSON * object loaded from an external account credentials file. @@ -190,7 +270,8 @@ export abstract class BaseExternalAccountClient extends AuthClient { options as BaseExternalAccountClientOptions ); - if (opts.get('type') !== EXTERNAL_ACCOUNT_TYPE) { + const type = opts.get('type'); + if (type && type !== EXTERNAL_ACCOUNT_TYPE) { throw new Error( `Expected "${EXTERNAL_ACCOUNT_TYPE}" type but ` + `received "${options.type}"` @@ -199,7 +280,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { const clientId = opts.get('client_id'); const clientSecret = opts.get('client_secret'); - const tokenUrl = opts.get('token_url'); + const tokenUrl = + opts.get('token_url') ?? + DEFAULT_TOKEN_URL.replace('{universeDomain}', this.universeDomain); const subjectTokenType = opts.get('subject_token_type'); const workforcePoolUserProject = opts.get('workforce_pool_user_project'); const serviceAccountImpersonationUrl = opts.get( @@ -254,6 +337,11 @@ export abstract class BaseExternalAccountClient extends AuthClient { } this.projectNumber = this.getProjectNumber(this.audience); + this.supplierContext = { + audience: this.audience, + subjectTokenType: this.subjectTokenType, + transporter: this.transporter, + }; } /** The service account email to be impersonated, if available. */ diff --git a/src/auth/defaultawssecuritycredentialssupplier.ts b/src/auth/defaultawssecuritycredentialssupplier.ts new file mode 100644 index 00000000..12d48d3f --- /dev/null +++ b/src/auth/defaultawssecuritycredentialssupplier.ts @@ -0,0 +1,272 @@ +// Copyright 2024 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. + +import {ExternalAccountSupplierContext} from './baseexternalclient'; +import {Gaxios, GaxiosOptions} from 'gaxios'; +import {Transporter} from '../transporters'; +import {AwsSecurityCredentialsSupplier} from './awsclient'; +import {AwsSecurityCredentials} from './awsrequestsigner'; +import {Headers} from './oauth2client'; + +/** + * Interface defining the AWS security-credentials endpoint response. + */ +interface AwsSecurityCredentialsResponse { + Code: string; + LastUpdated: string; + Type: string; + AccessKeyId: string; + SecretAccessKey: string; + Token: string; + Expiration: string; +} + +/** + * Interface defining the options used to build a {@link DefaultAwsSecurityCredentialsSupplier}. + */ +export interface DefaultAwsSecurityCredentialsSupplierOptions { + /** + * The URL to call to retrieve the active AWS region. + **/ + regionUrl?: string; + /** + * The URL to call to retrieve AWS security credentials. + **/ + securityCredentialsUrl?: string; + /** + ** The URL to call to retrieve the IMDSV2 session token. + **/ + imdsV2SessionTokenUrl?: string; + /** + * Additional Gaxios options to use when making requests to the AWS metadata + * endpoints. + */ + additionalGaxiosOptions?: GaxiosOptions; +} + +/** + * Internal AWS security credentials supplier implementation used by {@link AwsClient} + * when a credential source is provided instead of a user defined supplier. + * The logic is summarized as: + * 1. If imdsv2_session_token_url is provided in the credential source, then + * fetch the aws session token and include it in the headers of the + * metadata requests. This is a requirement for IDMSv2 but optional + * for IDMSv1. + * 2. Retrieve AWS region from availability-zone. + * 3a. Check AWS credentials in environment variables. If not found, get + * from security-credentials endpoint. + * 3b. Get AWS credentials from security-credentials endpoint. In order + * to retrieve this, the AWS role needs to be determined by calling + * security-credentials endpoint without any argument. Then the + * credentials can be retrieved via: security-credentials/role_name + * 4. Generate the signed request to AWS STS GetCallerIdentity action. + * 5. Inject x-goog-cloud-target-resource into header and serialize the + * signed request. This will be the subject-token to pass to GCP STS. + */ +export class DefaultAwsSecurityCredentialsSupplier + implements AwsSecurityCredentialsSupplier +{ + private readonly regionUrl?: string; + private readonly securityCredentialsUrl?: string; + private readonly imdsV2SessionTokenUrl?: string; + private readonly additionalGaxiosOptions?: GaxiosOptions; + + /** + * Instantiates a new DefaultAwsSecurityCredentialsSupplier using information + * from the credential_source stored in the ADC file. + * @param opts The default aws security credentials supplier options object to + * build the supplier with. + */ + constructor(opts: DefaultAwsSecurityCredentialsSupplierOptions) { + this.regionUrl = opts.regionUrl; + this.securityCredentialsUrl = opts.securityCredentialsUrl; + this.imdsV2SessionTokenUrl = opts.imdsV2SessionTokenUrl; + this.additionalGaxiosOptions = opts.additionalGaxiosOptions; + } + + /** + * Returns the active AWS region. This first checks to see if the region + * is available as an environment variable. If it is not, then the supplier + * will call the region URL. + * @param context {@link ExternalAccountSupplierContext} from the calling + * {@link AwsClient}, contains the requested audience and subject token type + * for the external account identity. + * @return A promise that resolves with the AWS region string. + */ + async getAwsRegion(context: ExternalAccountSupplierContext): Promise { + // Priority order for region determination: + // AWS_REGION > AWS_DEFAULT_REGION > metadata server. + if (this.#regionFromEnv) { + return this.#regionFromEnv; + } + + const metadataHeaders: Headers = {}; + if (!this.#regionFromEnv && this.imdsV2SessionTokenUrl) { + metadataHeaders['x-aws-ec2-metadata-token'] = + await this.#getImdsV2SessionToken(context.transporter); + } + if (!this.regionUrl) { + throw new Error( + 'Unable to determine AWS region due to missing ' + + '"options.credential_source.region_url"' + ); + } + const opts: GaxiosOptions = { + ...this.additionalGaxiosOptions, + url: this.regionUrl, + method: 'GET', + responseType: 'text', + headers: metadataHeaders, + }; + const response = await context.transporter.request(opts); + // Remove last character. For example, if us-east-2b is returned, + // the region would be us-east-2. + return response.data.substr(0, response.data.length - 1); + } + + /** + * Returns AWS security credentials. This first checks to see if the credentials + * is available as environment variables. If it is not, then the supplier + * will call the security credentials URL. + * @param context {@link ExternalAccountSupplierContext} from the calling + * {@link AwsClient}, contains the requested audience and subject token type + * for the external account identity. + * @return A promise that resolves with the AWS security credentials. + */ + async getAwsSecurityCredentials( + context: ExternalAccountSupplierContext + ): Promise { + // Check environment variables for permanent credentials first. + // https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html + if (this.#securityCredentialsFromEnv) { + return this.#securityCredentialsFromEnv; + } + + const metadataHeaders: Headers = {}; + if (this.imdsV2SessionTokenUrl) { + metadataHeaders['x-aws-ec2-metadata-token'] = + await this.#getImdsV2SessionToken(context.transporter); + } + // Since the role on a VM can change, we don't need to cache it. + const roleName = await this.#getAwsRoleName( + metadataHeaders, + context.transporter + ); + // Temporary credentials typically last for several hours. + // Expiration is returned in response. + // Consider future optimization of this logic to cache AWS tokens + // until their natural expiration. + const awsCreds = await this.#retrieveAwsSecurityCredentials( + roleName, + metadataHeaders, + context.transporter + ); + return { + accessKeyId: awsCreds.AccessKeyId, + secretAccessKey: awsCreds.SecretAccessKey, + token: awsCreds.Token, + }; + } + + /** + * @param transporter The transporter to use for requests. + * @return A promise that resolves with the IMDSv2 Session Token. + */ + async #getImdsV2SessionToken( + transporter: Transporter | Gaxios + ): Promise { + const opts: GaxiosOptions = { + ...this.additionalGaxiosOptions, + url: this.imdsV2SessionTokenUrl, + method: 'PUT', + responseType: 'text', + headers: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }; + const response = await transporter.request(opts); + return response.data; + } + + /** + * @param headers The headers to be used in the metadata request. + * @param transporter The transporter to use for requests. + * @return A promise that resolves with the assigned role to the current + * AWS VM. This is needed for calling the security-credentials endpoint. + */ + async #getAwsRoleName( + headers: Headers, + transporter: Transporter | Gaxios + ): Promise { + if (!this.securityCredentialsUrl) { + throw new Error( + 'Unable to determine AWS role name due to missing ' + + '"options.credential_source.url"' + ); + } + const opts: GaxiosOptions = { + ...this.additionalGaxiosOptions, + url: this.securityCredentialsUrl, + method: 'GET', + responseType: 'text', + headers: headers, + }; + const response = await transporter.request(opts); + return response.data; + } + + /** + * Retrieves the temporary AWS credentials by calling the security-credentials + * endpoint as specified in the `credential_source` object. + * @param roleName The role attached to the current VM. + * @param headers The headers to be used in the metadata request. + * @param transporter The transporter to use for requests. + * @return A promise that resolves with the temporary AWS credentials + * needed for creating the GetCallerIdentity signed request. + */ + async #retrieveAwsSecurityCredentials( + roleName: string, + headers: Headers, + transporter: Transporter | Gaxios + ): Promise { + const response = await transporter.request({ + ...this.additionalGaxiosOptions, + url: `${this.securityCredentialsUrl}/${roleName}`, + responseType: 'json', + headers: headers, + }); + return response.data; + } + + get #regionFromEnv(): string | null { + // The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. + // Only one is required. + return ( + process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'] || null + ); + } + + get #securityCredentialsFromEnv(): AwsSecurityCredentials | null { + // Both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required. + if ( + process.env['AWS_ACCESS_KEY_ID'] && + process.env['AWS_SECRET_ACCESS_KEY'] + ) { + return { + accessKeyId: process.env['AWS_ACCESS_KEY_ID'], + secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'], + token: process.env['AWS_SESSION_TOKEN'], + }; + } + return null; + } +} diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index e338164c..24a480c0 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -39,6 +39,7 @@ import { */ export const EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE = 'external_account_authorized_user'; +const DEFAULT_TOKEN_URL = 'https://sts.{universeDomain}/v1/oauthtoken'; /** * External Account Authorized User Credentials JSON interface. @@ -172,6 +173,9 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { additionalOptions?: AuthClientOptions ) { super({...options, ...additionalOptions}); + if (options.universe_domain) { + this.universeDomain = options.universe_domain; + } this.refreshToken = options.refresh_token; const clientAuth = { confidentialClientType: 'basic', @@ -180,7 +184,8 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { } as ClientAuthentication; this.externalAccountAuthorizedUserHandler = new ExternalAccountAuthorizedUserHandler( - options.token_url, + options.token_url ?? + DEFAULT_TOKEN_URL.replace('{universeDomain}', this.universeDomain), this.transporter, clientAuth ); @@ -198,10 +203,6 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { .eagerRefreshThresholdMillis as number; } this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; - - if (options.universe_domain) { - this.universeDomain = options.universe_domain; - } } async getAccessToken(): Promise<{ diff --git a/src/auth/filesubjecttokensupplier.ts b/src/auth/filesubjecttokensupplier.ts new file mode 100644 index 00000000..8882980c --- /dev/null +++ b/src/auth/filesubjecttokensupplier.ts @@ -0,0 +1,114 @@ +// Copyright 2024 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. + +import {ExternalAccountSupplierContext} from './baseexternalclient'; +import { + SubjectTokenFormatType, + SubjectTokenJsonResponse, + SubjectTokenSupplier, +} from './identitypoolclient'; +import {promisify} from 'util'; +import * as fs from 'fs'; + +// fs.readfile is undefined in browser karma tests causing +// `npm run browser-test` to fail as test.oauth2.ts imports this file via +// src/index.ts. +// Fallback to void function to avoid promisify throwing a TypeError. +const readFile = promisify(fs.readFile ?? (() => {})); +const realpath = promisify(fs.realpath ?? (() => {})); +const lstat = promisify(fs.lstat ?? (() => {})); + +/** + * Interface that defines options used to build a {@link FileSubjectTokenSupplier} + */ +export interface FileSubjectTokenSupplierOptions { + /** + * The file path where the external credential is located. + */ + filePath: string; + /** + * The token file or URL response type (JSON or text). + */ + formatType: SubjectTokenFormatType; + /** + * For JSON response types, this is the subject_token field name. For Azure, + * this is access_token. For text response types, this is ignored. + */ + subjectTokenFieldName?: string; +} + +/** + * Internal subject token supplier implementation used when a file location + * is configured in the credential configuration used to build an {@link IdentityPoolClient} + */ +export class FileSubjectTokenSupplier implements SubjectTokenSupplier { + private readonly filePath: string; + private readonly formatType: SubjectTokenFormatType; + private readonly subjectTokenFieldName?: string; + + /** + * Instantiates a new file based subject token supplier. + * @param opts The file subject token supplier options to build the supplier + * with. + */ + constructor(opts: FileSubjectTokenSupplierOptions) { + this.filePath = opts.filePath; + this.formatType = opts.formatType; + this.subjectTokenFieldName = opts.subjectTokenFieldName; + } + + /** + * Returns the subject token stored at the file specified in the constructor. + * @param context {@link ExternalAccountSupplierContext} from the calling + * {@link IdentityPoolClient}, contains the requested audience and subject + * token type for the external account identity. Not used. + */ + async getSubjectToken( + context: ExternalAccountSupplierContext + ): Promise { + // Make sure there is a file at the path. lstatSync will throw if there is + // nothing there. + let parsedFilePath = this.filePath; + try { + // Resolve path to actual file in case of symlink. Expect a thrown error + // if not resolvable. + parsedFilePath = await realpath(parsedFilePath); + + if (!(await lstat(parsedFilePath)).isFile()) { + throw new Error(); + } + } catch (err) { + if (err instanceof Error) { + err.message = `The file at ${parsedFilePath} does not exist, or it is not a file. ${err.message}`; + } + + throw err; + } + + let subjectToken: string | undefined; + const rawText = await readFile(parsedFilePath, {encoding: 'utf8'}); + if (this.formatType === 'text') { + subjectToken = rawText; + } else if (this.formatType === 'json' && this.subjectTokenFieldName) { + const json = JSON.parse(rawText) as SubjectTokenJsonResponse; + subjectToken = json[this.subjectTokenFieldName]; + } + if (!subjectToken) { + throw new Error( + 'Unable to parse the subject_token from the credential_source file' + ); + } + return subjectToken; + } +} diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index 0e89f544..160dce03 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -12,48 +12,89 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {GaxiosOptions} from 'gaxios'; -import * as fs from 'fs'; -import {promisify} from 'util'; - import { BaseExternalAccountClient, BaseExternalAccountClientOptions, + ExternalAccountSupplierContext, } from './baseexternalclient'; import {AuthClientOptions} from './authclient'; import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; +import {FileSubjectTokenSupplier} from './filesubjecttokensupplier'; +import {UrlSubjectTokenSupplier} from './urlsubjecttokensupplier'; -// fs.readfile is undefined in browser karma tests causing -// `npm run browser-test` to fail as test.oauth2.ts imports this file via -// src/index.ts. -// Fallback to void function to avoid promisify throwing a TypeError. -const readFile = promisify(fs.readFile ?? (() => {})); -const realpath = promisify(fs.realpath ?? (() => {})); -const lstat = promisify(fs.lstat ?? (() => {})); - -type SubjectTokenFormatType = 'json' | 'text'; +export type SubjectTokenFormatType = 'json' | 'text'; -interface SubjectTokenJsonResponse { +export interface SubjectTokenJsonResponse { [key: string]: string; } +/** + * Supplier interface for subject tokens. This can be implemented to + * return a subject token which can then be exchanged for a GCP token by an + * {@link IdentityPoolClient}. + */ +export interface SubjectTokenSupplier { + /** + * Gets a valid subject token for the requested external account identity. + * Note that these are not cached by the calling {@link IdentityPoolClient}, + * so caching should be including in the implementation. + * @param context {@link ExternalAccountSupplierContext} from the calling + * {@link IdentityPoolClient}, contains the requested audience and subject token type + * for the external account identity as well as the transport from the + * calling client to use for requests. + * @return A promise that resolves with the requested subject token string. + */ + getSubjectToken: (context: ExternalAccountSupplierContext) => Promise; +} + /** * Url-sourced/file-sourced credentials json interface. * This is used for K8s and Azure workloads. */ export interface IdentityPoolClientOptions extends BaseExternalAccountClientOptions { - credential_source: { + /** + * Object containing options to retrieve identity pool credentials. A valid credential + * source or a subject token supplier must be specified. + */ + credential_source?: { + /** + * The file location to read the subject token from. Either this or a URL + * should be specified. + */ file?: string; + /** + * The URL to call to retrieve the subject token. Either this or a file + * location should be specified. + */ url?: string; + /** + * Optional headers to send on the request to the specified URL. + */ headers?: { [key: string]: string; }; + /** + * The format that the subject token is in the file or the URL response. + * If not provided, will default to reading the text string directly. + */ format?: { + /** + * The format type. Can either be 'text' or 'json'. + */ type: SubjectTokenFormatType; + /** + * The field name containing the subject token value if the type is 'json'. + */ subject_token_field_name?: string; }; }; + /** + * The subject token supplier to call to retrieve the subject token to exchange + * for a GCP access token. Either this or a valid credential source should + * be specified. + */ + subject_token_supplier?: SubjectTokenSupplier; } /** @@ -61,11 +102,7 @@ export interface IdentityPoolClientOptions * used for K8s and Azure workloads. */ export class IdentityPoolClient extends BaseExternalAccountClient { - private readonly file?: string; - private readonly url?: string; - private readonly headers?: {[key: string]: string}; - private readonly formatType: SubjectTokenFormatType; - private readonly formatSubjectTokenFieldName?: string; + private readonly subjectTokenSupplier: SubjectTokenSupplier; /** * Instantiate an IdentityPoolClient instance using the provided JSON @@ -91,160 +128,82 @@ export class IdentityPoolClient extends BaseExternalAccountClient { const opts = originalOrCamelOptions(options as IdentityPoolClientOptions); const credentialSource = opts.get('credential_source'); - const credentialSourceOpts = originalOrCamelOptions(credentialSource); - - this.file = credentialSourceOpts.get('file'); - this.url = credentialSourceOpts.get('url'); - this.headers = credentialSourceOpts.get('headers'); - if (this.file && this.url) { + const subjectTokenSupplier = opts.get('subject_token_supplier'); + // Validate credential sourcing configuration. + if (!credentialSource && !subjectTokenSupplier) { throw new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.' + 'A credential source or subject token supplier must be specified.' ); - } else if (this.file && !this.url) { - this.credentialSourceType = 'file'; - } else if (!this.file && this.url) { - this.credentialSourceType = 'url'; - } else { + } + if (credentialSource && subjectTokenSupplier) { throw new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.' + 'Only one of credential source or subject token supplier can be specified.' ); } - const formatOpts = originalOrCamelOptions( - credentialSourceOpts.get('format') - ); - - // Text is the default format type. - this.formatType = formatOpts.get('type') || 'text'; - this.formatSubjectTokenFieldName = formatOpts.get( - 'subject_token_field_name' - ); + if (subjectTokenSupplier) { + this.subjectTokenSupplier = subjectTokenSupplier; + this.credentialSourceType = 'programmatic'; + } else { + const credentialSourceOpts = originalOrCamelOptions(credentialSource); - if (this.formatType !== 'json' && this.formatType !== 'text') { - throw new Error(`Invalid credential_source format "${this.formatType}"`); - } - if (this.formatType === 'json' && !this.formatSubjectTokenFieldName) { - throw new Error( - 'Missing subject_token_field_name for JSON credential_source format' + const formatOpts = originalOrCamelOptions( + credentialSourceOpts.get('format') ); - } - } - /** - * Triggered when a external subject token is needed to be exchanged for a GCP - * access token via GCP STS endpoint. - * This uses the `options.credential_source` object to figure out how - * to retrieve the token using the current environment. In this case, - * this either retrieves the local credential from a file location (k8s - * workload) or by sending a GET request to a local metadata server (Azure - * workloads). - * @return A promise that resolves with the external subject token. - */ - async retrieveSubjectToken(): Promise { - if (this.file) { - return await this.getTokenFromFile( - this.file!, - this.formatType, - this.formatSubjectTokenFieldName + // Text is the default format type. + const formatType = formatOpts.get('type') || 'text'; + const formatSubjectTokenFieldName = formatOpts.get( + 'subject_token_field_name' ); - } - return await this.getTokenFromUrl( - this.url!, - this.formatType, - this.formatSubjectTokenFieldName, - this.headers - ); - } - /** - * Looks up the external subject token in the file path provided and - * resolves with that token. - * @param file The file path where the external credential is located. - * @param formatType The token file or URL response type (JSON or text). - * @param formatSubjectTokenFieldName For JSON response types, this is the - * subject_token field name. For Azure, this is access_token. For text - * response types, this is ignored. - * @return A promise that resolves with the external subject token. - */ - private async getTokenFromFile( - filePath: string, - formatType: SubjectTokenFormatType, - formatSubjectTokenFieldName?: string - ): Promise { - // 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 = await realpath(filePath); - - if (!(await lstat(filePath)).isFile()) { - throw new Error(); + if (formatType !== 'json' && formatType !== 'text') { + throw new Error(`Invalid credential_source format "${formatType}"`); } - } catch (err) { - if (err instanceof Error) { - err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`; + if (formatType === 'json' && !formatSubjectTokenFieldName) { + throw new Error( + 'Missing subject_token_field_name for JSON credential_source format' + ); } - throw err; - } - - let subjectToken: string | undefined; - const rawText = await readFile(filePath, {encoding: 'utf8'}); - if (formatType === 'text') { - subjectToken = rawText; - } else if (formatType === 'json' && formatSubjectTokenFieldName) { - const json = JSON.parse(rawText) as SubjectTokenJsonResponse; - subjectToken = json[formatSubjectTokenFieldName]; - } - if (!subjectToken) { - throw new Error( - 'Unable to parse the subject_token from the credential_source file' - ); + const file = credentialSourceOpts.get('file'); + const url = credentialSourceOpts.get('url'); + const headers = credentialSourceOpts.get('headers'); + if (file && url) { + throw new Error( + 'No valid Identity Pool "credential_source" provided, must be either file or url.' + ); + } else if (file && !url) { + this.credentialSourceType = 'file'; + this.subjectTokenSupplier = new FileSubjectTokenSupplier({ + filePath: file, + formatType: formatType, + subjectTokenFieldName: formatSubjectTokenFieldName, + }); + } else if (!file && url) { + this.credentialSourceType = 'url'; + this.subjectTokenSupplier = new UrlSubjectTokenSupplier({ + url: url, + formatType: formatType, + subjectTokenFieldName: formatSubjectTokenFieldName, + headers: headers, + additionalGaxiosOptions: IdentityPoolClient.RETRY_CONFIG, + }); + } else { + throw new Error( + 'No valid Identity Pool "credential_source" provided, must be either file or url.' + ); + } } - return subjectToken; } /** - * Sends a GET request to the URL provided and resolves with the returned - * external subject token. - * @param url The URL to call to retrieve the subject token. This is typically - * a local metadata server. - * @param formatType The token file or URL response type (JSON or text). - * @param formatSubjectTokenFieldName For JSON response types, this is the - * subject_token field name. For Azure, this is access_token. For text - * response types, this is ignored. - * @param headers The optional additional headers to send with the request to - * the metadata server url. + * Triggered when a external subject token is needed to be exchanged for a GCP + * access token via GCP STS endpoint. Gets a subject token by calling + * the configured {@link SubjectTokenSupplier} * @return A promise that resolves with the external subject token. */ - private async getTokenFromUrl( - url: string, - formatType: SubjectTokenFormatType, - formatSubjectTokenFieldName?: string, - headers?: {[key: string]: string} - ): Promise { - const opts: GaxiosOptions = { - ...IdentityPoolClient.RETRY_CONFIG, - url, - method: 'GET', - headers, - responseType: formatType, - }; - let subjectToken: string | undefined; - if (formatType === 'text') { - const response = await this.transporter.request(opts); - subjectToken = response.data; - } else if (formatType === 'json' && formatSubjectTokenFieldName) { - const response = - await this.transporter.request(opts); - subjectToken = response.data[formatSubjectTokenFieldName]; - } - if (!subjectToken) { - throw new Error( - 'Unable to parse the subject_token from the credential_source URL' - ); - } - return subjectToken; + async retrieveSubjectToken(): Promise { + return this.subjectTokenSupplier.getSubjectToken(this.supplierContext); } } diff --git a/src/auth/urlsubjecttokensupplier.ts b/src/auth/urlsubjecttokensupplier.ts new file mode 100644 index 00000000..9ae01f75 --- /dev/null +++ b/src/auth/urlsubjecttokensupplier.ts @@ -0,0 +1,108 @@ +// Copyright 2024 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. + +import {ExternalAccountSupplierContext} from './baseexternalclient'; +import {GaxiosOptions} from 'gaxios'; +import { + SubjectTokenFormatType, + SubjectTokenJsonResponse, + SubjectTokenSupplier, +} from './identitypoolclient'; + +/** + * Interface that defines options used to build a {@link UrlSubjectTokenSupplier} + */ +export interface UrlSubjectTokenSupplierOptions { + /** + * The URL to call to retrieve the subject token. This is typically a local + * metadata server. + */ + url: string; + /** + * The token file or URL response type (JSON or text). + */ + formatType: SubjectTokenFormatType; + /** + * For JSON response types, this is the subject_token field name. For Azure, + * this is access_token. For text response types, this is ignored. + */ + subjectTokenFieldName?: string; + /** + * The optional additional headers to send with the request to the metadata + * server url. + */ + headers?: {[key: string]: string}; + /** + * Additional gaxios options to use for the request to the specified URL. + */ + additionalGaxiosOptions?: GaxiosOptions; +} + +/** + * Internal subject token supplier implementation used when a URL + * is configured in the credential configuration used to build an {@link IdentityPoolClient} + */ +export class UrlSubjectTokenSupplier implements SubjectTokenSupplier { + private readonly url: string; + private readonly headers?: {[key: string]: string}; + private readonly formatType: SubjectTokenFormatType; + private readonly subjectTokenFieldName?: string; + private readonly additionalGaxiosOptions?: GaxiosOptions; + + /** + * Instantiates a URL subject token supplier. + * @param opts The URL subject token supplier options to build the supplier with. + */ + constructor(opts: UrlSubjectTokenSupplierOptions) { + this.url = opts.url; + this.formatType = opts.formatType; + this.subjectTokenFieldName = opts.subjectTokenFieldName; + this.headers = opts.headers; + this.additionalGaxiosOptions = opts.additionalGaxiosOptions; + } + + /** + * Sends a GET request to the URL provided in the constructor and resolves + * with the returned external subject token. + * @param context {@link ExternalAccountSupplierContext} from the calling + * {@link IdentityPoolClient}, contains the requested audience and subject + * token type for the external account identity. Not used. + */ + async getSubjectToken( + context: ExternalAccountSupplierContext + ): Promise { + const opts: GaxiosOptions = { + ...this.additionalGaxiosOptions, + url: this.url, + method: 'GET', + headers: this.headers, + responseType: this.formatType, + }; + let subjectToken: string | undefined; + if (this.formatType === 'text') { + const response = await context.transporter.request(opts); + subjectToken = response.data; + } else if (this.formatType === 'json' && this.subjectTokenFieldName) { + const response = + await context.transporter.request(opts); + subjectToken = response.data[this.subjectTokenFieldName]; + } + if (!subjectToken) { + throw new Error( + 'Unable to parse the subject_token from the credential_source URL' + ); + } + return subjectToken; + } +} diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index fb00cadd..db05785b 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -16,9 +16,12 @@ import * as assert from 'assert'; import {describe, it, afterEach, beforeEach} from 'mocha'; import * as nock from 'nock'; import * as sinon from 'sinon'; -import {AwsClient} from '../src/auth/awsclient'; +import {AwsClient, AwsSecurityCredentialsSupplier} from '../src/auth/awsclient'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; -import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; +import { + BaseExternalAccountClient, + ExternalAccountSupplierContext, +} from '../src/auth/baseexternalclient'; import { assertGaxiosResponsePresent, getAudience, @@ -28,6 +31,7 @@ import { mockStsTokenExchange, getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; +import {AwsSecurityCredentials} from '../src/auth/awsrequestsigner'; nock.disableNetConnect(); @@ -265,6 +269,36 @@ describe('AwsClient', () => { assert.throws(() => new AwsClient(invalidOptions), expectedError); }); + it('should throw when both a credential source and supplier are provided', () => { + const expectedError = new Error( + 'Only one of credential source or AWS security credentials supplier can be specified.' + ); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: awsCredentialSource, + aws_security_credentials_supplier: new TestAwsSupplier({}), + }; + + assert.throws(() => new AwsClient(invalidOptions), expectedError); + }); + + it('should throw when neither a credential source or supplier are provided', () => { + const expectedError = new Error( + 'A credential source or AWS security credentials supplier must be specified.' + ); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + }; + + assert.throws(() => new AwsClient(invalidOptions), expectedError); + }); + it('should not throw when valid AWS options are provided', () => { assert.doesNotThrow(() => { return new AwsClient(awsOptions); @@ -1042,4 +1076,239 @@ describe('AwsClient', () => { }); }); }); + + describe('for custom supplier retrieved tokens', () => { + describe('retrieveSubjectToken()', () => { + it('should resolve on success for permanent creds', async () => { + const supplier = new TestAwsSupplier({ + credentials: { + accessKeyId: accessKeyId, + secretAccessKey: secretAccessKey, + }, + region: awsRegion, + }); + const options = { + aws_security_credentials_supplier: supplier, + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + }; + + const client = new AwsClient(options); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectTokenNoToken); + }); + + it('should resolve on success for temporary creds', async () => { + const supplier = new TestAwsSupplier({ + credentials: { + accessKeyId: accessKeyId, + secretAccessKey: secretAccessKey, + token: token, + }, + region: awsRegion, + }); + const options = { + aws_security_credentials_supplier: supplier, + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + }; + + const client = new AwsClient(options); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + }); + + it('should reject when getAwsRegion() throws an error', async () => { + const expectedError = new Error('expected error message'); + const supplier = new TestAwsSupplier({ + regionError: expectedError, + }); + const options = { + aws_security_credentials_supplier: supplier, + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + }; + + const client = new AwsClient(options); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should reject when getAwsSecurityCredentials() throws an error', async () => { + const expectedError = new Error('expected error message'); + const supplier = new TestAwsSupplier({ + region: awsRegion, + credentialsError: expectedError, + }); + const options = { + aws_security_credentials_supplier: supplier, + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + }; + + const client = new AwsClient(options); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + }); + + describe('getAccessToken()', () => { + it('should resolve on retrieveSubjectToken success', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: expectedSubjectTokenNoToken, + subject_token_type: + 'urn:ietf:params:aws:token-type:aws4_request', + }, + }, + ]) + ); + const supplier = new TestAwsSupplier({ + credentials: { + accessKeyId: accessKeyId, + secretAccessKey: secretAccessKey, + }, + region: awsRegion, + }); + const options = { + aws_security_credentials_supplier: supplier, + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + }; + + const client = new AwsClient(options); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should reject on retrieveSubjectToken error', async () => { + const expectedError = new Error('expected error message'); + const supplier = new TestAwsSupplier({ + region: awsRegion, + credentialsError: expectedError, + }); + const options = { + aws_security_credentials_supplier: supplier, + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + }; + + const client = new AwsClient(options); + + await assert.rejects(client.getAccessToken(), expectedError); + }); + + it('should set x-goog-api-client header correctly', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: expectedSubjectTokenNoToken, + subject_token_type: + 'urn:ietf:params:aws:token-type:aws4_request', + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'programmatic', + false, + false + ), + } + ) + ); + const supplier = new TestAwsSupplier({ + credentials: { + accessKeyId: accessKeyId, + secretAccessKey: secretAccessKey, + }, + region: awsRegion, + }); + const options = { + aws_security_credentials_supplier: supplier, + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + }; + + const client = new AwsClient(options); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); + }); + }); }); + +interface TestAwsSupplierOptions { + credentials?: AwsSecurityCredentials; + region?: string; + credentialsError?: Error; + regionError?: Error; +} + +class TestAwsSupplier implements AwsSecurityCredentialsSupplier { + private readonly credentials?: AwsSecurityCredentials; + private readonly region?: string; + private readonly credentialsError?: Error; + private readonly regionError?: Error; + + constructor(options: TestAwsSupplierOptions) { + this.credentials = options.credentials; + this.region = options.region; + this.credentialsError = options.credentialsError; + this.regionError = options.regionError; + } + + async getAwsRegion(context: ExternalAccountSupplierContext): Promise { + if (this.regionError) { + throw this.regionError; + } else { + return this.region ?? ''; + } + } + + async getAwsSecurityCredentials( + context: ExternalAccountSupplierContext + ): Promise { + if (this.credentialsError) { + throw this.credentialsError; + } else { + return this.credentials ?? {accessKeyId: '', secretAccessKey: ''}; + } + } +} diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 96ea57ce..a0974a85 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -82,6 +82,14 @@ describe('BaseExternalAccountClient', () => { file: '/var/run/secrets/goog.id/token', }, }; + const externalAccountOptionsNoUrl = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + credential_source: { + file: '/var/run/secrets/goog.id/token', + }, + }; const externalAccountOptionsWithCreds = { type: 'external_account', audience, @@ -262,6 +270,62 @@ describe('BaseExternalAccountClient', () => { refreshOptions.eagerRefreshThresholdMillis ); }); + + it('should set default token url', async () => { + const client = new TestExternalAccountClient(externalAccountOptionsNoUrl); + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + await client.getAccessToken(); + + scope.done(); + }); + + it('should set universe domain on default token url', async () => { + const options: BaseExternalAccountClientOptions = { + ...externalAccountOptionsNoUrl, + universe_domain: 'test.com', + }; + + const client = new TestExternalAccountClient(options); + + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ], + {}, + 'https://sts.test.com' + ); + + await client.getAccessToken(); + + scope.done(); + }); }); describe('projectNumber', () => { diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 5d0e7920..7b74ebc6 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -95,6 +95,14 @@ describe('ExternalAccountAuthorizedUserClient', () => { token_url: TOKEN_REFRESH_URL, token_info_url: TOKEN_INFO_URL, } as ExternalAccountAuthorizedUserClientOptions; + const externalAccountAuthorizedUserCredentialOptionsNoToken = { + type: EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, + audience: audience, + client_id: 'clientId', + client_secret: 'clientSecret', + refresh_token: 'refreshToken', + token_info_url: TOKEN_INFO_URL, + } as ExternalAccountAuthorizedUserClientOptions; const successfulRefreshResponse = { access_token: 'newAccessToken', refresh_token: 'newRefreshToken', @@ -133,6 +141,44 @@ describe('ExternalAccountAuthorizedUserClient', () => { assert(client.eagerRefreshThresholdMillis === EXPIRATION_TIME_OFFSET); }); + it('should set default token url', async () => { + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponse, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptionsNoToken + ); + await client.getAccessToken(); + scope.done(); + }); + + it('should set universe domain token url', async () => { + const scope = mockStsTokenRefresh('https://sts.test.com', REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponse, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + const client = new ExternalAccountAuthorizedUserClient({ + ...externalAccountAuthorizedUserCredentialOptionsNoToken, + universe_domain: 'test.com', + }); + await client.getAccessToken(); + scope.done(); + }); + it('should set custom RefreshOptions', () => { const refreshOptions = { eagerRefreshThresholdMillis: 5000, diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 1faa5bdd..ac9317c7 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -20,9 +20,13 @@ import {createCrypto} from '../src/crypto/crypto'; import { IdentityPoolClient, IdentityPoolClientOptions, + SubjectTokenSupplier, } from '../src/auth/identitypoolclient'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; -import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; +import { + BaseExternalAccountClient, + ExternalAccountSupplierContext, +} from '../src/auth/baseexternalclient'; import { assertGaxiosResponsePresent, getAudience, @@ -302,6 +306,42 @@ describe('IdentityPoolClient', () => { } ); + it('should throw when neither a credential source or a supplier is provided', () => { + const expectedError = new Error( + 'A credential source or subject token supplier must be specified.' + ); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new IdentityPoolClient(invalidOptions as any); + }, expectedError); + }); + + it('should throw when both a credential source and a supplier is provided', () => { + const expectedError = new Error( + 'Only one of credential source or subject token supplier can be specified.' + ); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: getTokenUrl(), + credential_source: {}, + subject_token_supplier: new TestSubjectTokenSupplier({}), + }; + + assert.throws(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new IdentityPoolClient(invalidOptions as any); + }, expectedError); + }); + it('should not throw when valid file-sourced options are provided', () => { assert.doesNotThrow(() => { return new IdentityPoolClient(fileSourcedOptions); @@ -314,10 +354,21 @@ describe('IdentityPoolClient', () => { }); }); + it('should not throw when subject token supplier is provided', () => { + const options = { + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + subject_token_supplier: new TestSubjectTokenSupplier({}), + }; + assert.doesNotThrow(() => { + return new IdentityPoolClient(options); + }); + }); + it('should not throw on headerless url-sourced options', () => { const urlSourcedOptionsNoHeaders = Object.assign({}, urlSourcedOptions); urlSourcedOptionsNoHeaders.credential_source = { - url: urlSourcedOptions.credential_source.url, + url: urlSourcedOptions.credential_source?.url, }; assert.doesNotThrow(() => { return new IdentityPoolClient(urlSourcedOptionsNoHeaders); @@ -865,7 +916,7 @@ describe('IdentityPoolClient', () => { // Create options without headers. const urlSourcedOptionsNoHeaders = Object.assign({}, urlSourcedOptions); urlSourcedOptionsNoHeaders.credential_source = { - url: urlSourcedOptions.credential_source.url, + url: urlSourcedOptions.credential_source?.url, }; const externalSubjectToken = 'SUBJECT_TOKEN_1'; const scope = nock(metadataBaseUrl) @@ -1150,4 +1201,219 @@ describe('IdentityPoolClient', () => { }); }); }); + + describe('for supplier-sourced subject tokens', () => { + describe('retrieveSubjectToken()', () => { + it('should resolve when the subject token is returned', async () => { + const options = { + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + subject_token_supplier: new TestSubjectTokenSupplier({ + subjectToken: 'TestTokenValue', + }), + }; + const client = new IdentityPoolClient(options); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, 'TestTokenValue'); + }); + + it('should return when the an error is returned', async () => { + const expectedError = new Error('Test error from supplier.'); + const options = { + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + subject_token_supplier: new TestSubjectTokenSupplier({ + error: expectedError, + }), + }; + const client = new IdentityPoolClient(options); + + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + }); + + describe('getAccessToken()', () => { + it('should resolve on retrieveSubjectToken success', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token retrieved from url should be used. + subject_token: externalSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]) + ); + + const options = { + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + subject_token_supplier: new TestSubjectTokenSupplier({ + subjectToken: externalSubjectToken, + }), + }; + const client = new IdentityPoolClient(options); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should handle service account access token', async () => { + const now = new Date().getTime(); + const saSuccessResponse = { + accessToken: 'SA_ACCESS_TOKEN', + expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), + }; + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token retrieved from url should be used. + subject_token: externalSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }) + ); + + const options = { + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + service_account_impersonation_url: + getServiceAccountImpersonationUrl(), + subject_token_supplier: new TestSubjectTokenSupplier({ + subjectToken: externalSubjectToken, + }), + }; + const client = new IdentityPoolClient(options); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: saSuccessResponse.accessToken, + }); + scopes.forEach(scope => scope.done()); + }); + + it('should reject with retrieveSubjectToken error', async () => { + const expectedError = new Error('Test error from supplier.'); + const options = { + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + subject_token_supplier: new TestSubjectTokenSupplier({ + error: expectedError, + }), + }; + const client = new IdentityPoolClient(options); + + await assert.rejects(client.getAccessToken(), expectedError); + }); + + it('should send the correct x-goog-api-client header value', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + // Subject token retrieved from url should be used. + subject_token: externalSubjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ], + { + 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( + 'programmatic', + false, + false + ), + } + ) + ); + + const options = { + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + subject_token_supplier: new TestSubjectTokenSupplier({ + subjectToken: externalSubjectToken, + }), + }; + const client = new IdentityPoolClient(options); + const actualResponse = await client.getAccessToken(); + + // Confirm raw GaxiosResponse appended to response. + assertGaxiosResponsePresent(actualResponse); + delete actualResponse.res; + assert.deepStrictEqual(actualResponse, { + token: stsSuccessfulResponse.access_token, + }); + scopes.forEach(scope => scope.done()); + }); + }); + }); }); + +interface TestSubjectTokenSupplierOptions { + subjectToken?: string; + error?: Error; +} + +class TestSubjectTokenSupplier implements SubjectTokenSupplier { + private readonly subjectToken: string; + private readonly error?: Error; + + constructor(options: TestSubjectTokenSupplierOptions) { + this.subjectToken = options.subjectToken ?? ''; + this.error = options.error; + } + + getSubjectToken(context: ExternalAccountSupplierContext): Promise { + if (this.error) { + throw this.error; + } + return Promise.resolve(this.subjectToken); + } +} From 3018f7c51d598be10ce13ff67a2a2de34cbd2c3d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:30:03 -0700 Subject: [PATCH 520/662] chore(main): release 9.9.0 (#1796) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42522f51..81daafbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.9.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.8.0...v9.9.0) (2024-04-18) + + +### Features + +* Adds suppliers for custom subject token and AWS credentials ([#1795](https://github.com/googleapis/google-auth-library-nodejs/issues/1795)) ([c680b5d](https://github.com/googleapis/google-auth-library-nodejs/commit/c680b5ddfa526d414ad1250bb6f5af69c498b909)) + ## [9.8.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.7.0...v9.8.0) (2024-04-12) diff --git a/package.json b/package.json index 4eb160a0..050f8d32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.8.0", + "version": "9.9.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index a2926e4f..e99b90a1 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^15.0.0", - "google-auth-library": "^9.8.0", + "google-auth-library": "^9.9.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From 34d1b2231f7b772474e7e750953591baae745235 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 30 Apr 2024 14:57:09 -0700 Subject: [PATCH 521/662] build: pin `@compodoc/compodoc` (#1805) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 050f8d32..2287cff8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "jws": "^4.0.0" }, "devDependencies": { - "@compodoc/compodoc": "^1.1.7", + "@compodoc/compodoc": "1.1.23", "@types/base64-js": "^1.2.5", "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", From 40406a0512cde1d75d2af7dd23aa7aa7de38d30b Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 1 May 2024 01:22:29 +0200 Subject: [PATCH 522/662] fix(deps): update dependency @googleapis/iam to v16 (#1803) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index e99b90a1..11047ce5 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^15.0.0", + "@googleapis/iam": "^16.0.0", "google-auth-library": "^9.9.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From 6014adec1b7b1e9abe6fa2fdd53e3231029f9129 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 1 May 2024 01:30:21 +0200 Subject: [PATCH 523/662] chore(deps): update actions/checkout digest to 0ad4b8f (#1797) Co-authored-by: Daniel Bankhead --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6290a497..48c94a96 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [14, 16, 18, 20] steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - uses: actions/setup-node@v4 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - uses: actions/setup-node@v4 with: node-version: 14 From 4d67f07380f690a99c8facf7266db7cb2d6c69b3 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 3 May 2024 20:53:35 +0200 Subject: [PATCH 524/662] fix(deps): update dependency @googleapis/iam to v17 (#1808) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 11047ce5..a9fa9f7e 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^16.0.0", + "@googleapis/iam": "^17.0.0", "google-auth-library": "^9.9.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From bb306ef2c07a3f4c32c76523cc630ac43e02ec38 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 7 May 2024 21:46:23 -0700 Subject: [PATCH 525/662] refactor: `universe_domain` endpoint to `universe-domain` (#1810) --- src/auth/googleauth.ts | 2 +- test/test.googleauth.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index f3faedac..40b71d2f 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -317,7 +317,7 @@ export class GoogleAuth { let universeDomain: string; try { - universeDomain = await gcpMetadata.universe('universe_domain'); + universeDomain = await gcpMetadata.universe('universe-domain'); universeDomain ||= DEFAULT_UNIVERSE; } catch (e) { if (e && (e as GaxiosError)?.response?.status === 404) { diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index ff3d5bcd..11be72f2 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -65,7 +65,7 @@ describe('googleauth', () => { const host = HOST_ADDRESS; const instancePath = `${BASE_PATH}/instance`; const svcAccountPath = `${instancePath}/service-accounts/default/email`; - const universeDomainPath = `${BASE_PATH}/universe/universe_domain`; + const universeDomainPath = `${BASE_PATH}/universe/universe-domain`; const API_KEY = 'test-123'; const PEM_PATH = './test/fixtures/private.pem'; const STUB_PROJECT = 'my-awesome-project'; From ae8bc5476f5d93c8516d9a9eb553e7ce7c00edd5 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 9 May 2024 20:26:49 -0700 Subject: [PATCH 526/662] feat: Implement `UserRefreshClient#fetchIdToken` (#1811) * feat: Implement `UserRefreshClient#fetchIdToken` * test: UserRefreshClient client for getIdTokenClient * refactor: Use `target_audience` > `audience` * refactor: Use `transporter` Removes redundant calls * test: Improve tests --- src/auth/refreshclient.ts | 23 +++++++++++- test/test.googleauth.ts | 74 +++++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index a53f1b1d..eca95d1b 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -13,12 +13,13 @@ // limitations under the License. import * as stream from 'stream'; -import {JWTInput} from './credentials'; +import {CredentialRequest, JWTInput} from './credentials'; import { GetTokenResponse, OAuth2Client, OAuth2ClientOptions, } from './oauth2client'; +import {stringify} from 'querystring'; export const USER_REFRESH_ACCOUNT_TYPE = 'authorized_user'; @@ -78,6 +79,26 @@ export class UserRefreshClient extends OAuth2Client { return super.refreshTokenNoCache(this._refreshToken); } + async fetchIdToken(targetAudience: string): Promise { + const res = await this.transporter.request({ + ...UserRefreshClient.RETRY_CONFIG, + url: this.endpoints.oauth2TokenUrl, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + data: stringify({ + client_id: this._clientId, + client_secret: this._clientSecret, + grant_type: 'refresh_token', + refresh_token: this._refreshToken, + target_audience: targetAudience, + }), + }); + + return res.data.id_token!; + } + /** * Create a UserRefreshClient credentials instance using the given input * options. diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 11be72f2..3bb2ce95 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -55,6 +55,7 @@ import { import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; import {AuthClient, DEFAULT_UNIVERSE} from '../src/auth/authclient'; import {ExternalAccountAuthorizedUserClient} from '../src/auth/externalAccountAuthorizedUserClient'; +import {stringify} from 'querystring'; nock.disableNetConnect(); @@ -1520,20 +1521,20 @@ describe('googleauth', () => { assert(client.idTokenProvider instanceof JWT); }); - it('should call getClient for getIdTokenClient', async () => { + it('should return a UserRefreshClient client for getIdTokenClient', async () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' + './test/fixtures/refresh.json' ); + mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id'); - const spy = sinon.spy(auth, 'getClient'); const client = await auth.getIdTokenClient('a-target-audience'); assert(client instanceof IdTokenClient); - assert(spy.calledOnce); + assert(client.idTokenProvider instanceof UserRefreshClient); }); - it('should fail when using UserRefreshClient', async () => { + it('should properly use `UserRefreshClient` client for `getIdTokenClient`', async () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', @@ -1541,16 +1542,59 @@ describe('googleauth', () => { ); mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id'); - try { - await auth.getIdTokenClient('a-target-audience'); - } catch (e) { - assert(e instanceof Error); - assert( - e.message.startsWith('Cannot fetch ID token in this environment') - ); - return; - } - assert.fail('failed to throw'); + // Assert `UserRefreshClient` + const baseClient = await auth.getClient(); + assert(baseClient instanceof UserRefreshClient); + + // Setup variables + const idTokenPayload = Buffer.from(JSON.stringify({exp: 100})).toString( + 'base64' + ); + const testIdToken = `TEST.${idTokenPayload}.TOKEN`; + const targetAudience = 'a-target-audience'; + const tokenEndpoint = new URL(baseClient.endpoints.oauth2TokenUrl); + const expectedTokenRequestBody = stringify({ + client_id: baseClient._clientId, + client_secret: baseClient._clientSecret, + grant_type: 'refresh_token', + refresh_token: baseClient._refreshToken, + target_audience: targetAudience, + }); + const url = new URL('https://my-protected-endpoint.a.app'); + const expectedRes = {hello: true}; + + // Setup mock endpoints + nock(tokenEndpoint.origin) + .post(tokenEndpoint.pathname, expectedTokenRequestBody) + .reply(200, {id_token: testIdToken}); + nock(url.origin, { + reqheaders: { + authorization: `Bearer ${testIdToken}`, + }, + }) + .get(url.pathname) + .reply(200, expectedRes); + + // Make assertions + const client = await auth.getIdTokenClient(targetAudience); + assert(client instanceof IdTokenClient); + assert(client.idTokenProvider instanceof UserRefreshClient); + + const res = await client.request({url}); + assert.deepStrictEqual(res.data, expectedRes); + }); + + it('should call getClient for getIdTokenClient', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json' + ); + + const spy = sinon.spy(auth, 'getClient'); + const client = await auth.getIdTokenClient('a-target-audience'); + assert(client instanceof IdTokenClient); + assert(spy.calledOnce); }); describe('getUniverseDomain', () => { From b2b9676f933c012fb2cd1789ad80b927af0de07c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 10 May 2024 08:05:41 +0200 Subject: [PATCH 527/662] fix(deps): update dependency @googleapis/iam to v18 (#1809) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index a9fa9f7e..eda392db 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^17.0.0", + "@googleapis/iam": "^18.0.0", "google-auth-library": "^9.9.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From 7dbedff829be093dad0a2da32f0ab12862d473bc Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 11:24:05 -0700 Subject: [PATCH 528/662] chore(main): release 9.10.0 (#1806) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81daafbc..4e81627c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.10.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.9.0...v9.10.0) (2024-05-10) + + +### Features + +* Implement `UserRefreshClient#fetchIdToken` ([#1811](https://github.com/googleapis/google-auth-library-nodejs/issues/1811)) ([ae8bc54](https://github.com/googleapis/google-auth-library-nodejs/commit/ae8bc5476f5d93c8516d9a9eb553e7ce7c00edd5)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v16 ([#1803](https://github.com/googleapis/google-auth-library-nodejs/issues/1803)) ([40406a0](https://github.com/googleapis/google-auth-library-nodejs/commit/40406a0512cde1d75d2af7dd23aa7aa7de38d30b)) +* **deps:** Update dependency @googleapis/iam to v17 ([#1808](https://github.com/googleapis/google-auth-library-nodejs/issues/1808)) ([4d67f07](https://github.com/googleapis/google-auth-library-nodejs/commit/4d67f07380f690a99c8facf7266db7cb2d6c69b3)) +* **deps:** Update dependency @googleapis/iam to v18 ([#1809](https://github.com/googleapis/google-auth-library-nodejs/issues/1809)) ([b2b9676](https://github.com/googleapis/google-auth-library-nodejs/commit/b2b9676f933c012fb2cd1789ad80b927af0de07c)) + ## [9.9.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.8.0...v9.9.0) (2024-04-18) diff --git a/package.json b/package.json index 2287cff8..d3b29631 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.9.0", + "version": "9.10.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index eda392db..d0684f68 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^18.0.0", - "google-auth-library": "^9.9.0", + "google-auth-library": "^9.10.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From d6c2b9cd2e5062bc98329327d2b565bba8caf15a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 21 May 2024 23:57:15 +0200 Subject: [PATCH 529/662] chore(deps): update dependency sinon to v18 (#1816) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3b29631..ff1bb864 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "nock": "^13.0.0", "null-loader": "^4.0.0", "puppeteer": "^21.0.0", - "sinon": "^15.0.0", + "sinon": "^18.0.0", "ts-loader": "^8.0.0", "typescript": "^5.1.6", "webpack": "^5.21.2", From 4615133a2fa5ac0190ad404b8e50bf4a96fd0051 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 22 May 2024 00:03:15 +0200 Subject: [PATCH 530/662] chore(deps): update dependency @types/sinon to v17 (#1815) Co-authored-by: Daniel Bankhead --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ff1bb864..0588d0f0 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", "@types/node": "^20.4.2", - "@types/sinon": "^10.0.0", + "@types/sinon": "^17.0.0", "assert-rejects": "^1.0.0", "c8": "^8.0.0", "chai": "^4.2.0", From d012a457fd024a7457378d81f6dcc214af874acc Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 22 May 2024 22:59:58 +0200 Subject: [PATCH 531/662] chore(deps): update actions/checkout digest to a5ac7e5 (#1818) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 48c94a96..23f2b874 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [14, 16, 18, 20] steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 - uses: actions/setup-node@v4 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 - uses: actions/setup-node@v4 with: node-version: 14 From 020236566f4445c48fe112d66d2a3b9a99e3e148 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 12:58:20 -0700 Subject: [PATCH 532/662] chore: [node] add auto-approve templates, and install dependencies with engines-strict (#1820) chore: add auto-approve templates, and install dependencies with engines-strict Source-Link: https://github.com/googleapis/synthtool/commit/4a02d97333d1c1642d1b19b00645afdcf4ab36a4 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:68e1cece0d6d3336c4f1cb9d2857b020af5574dff6da6349293d1c6d4eea82d8 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 6 +++--- .github/auto-approve.yml | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 638efabf..34bb2086 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:e92044720ab3cb6984a70b0c6001081204375959ba3599ef6c42dd99a7783a67 -# created: 2023-11-10T00:24:05.581078808Z + digest: sha256:68e1cece0d6d3336c4f1cb9d2857b020af5574dff6da6349293d1c6d4eea82d8 +# created: 2024-05-31T15:46:42.989947733Z diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml index 4cd91cc1..ec51b072 100644 --- a/.github/auto-approve.yml +++ b/.github/auto-approve.yml @@ -1,3 +1,4 @@ processes: - "NodeDependency" - - "OwlBotTemplateChanges" + - "OwlBotTemplateChangesNode" + - "OwlBotPRsNode" \ No newline at end of file From 4a14e8c3bdcfa9d8531a231b00b946728530ce12 Mon Sep 17 00:00:00 2001 From: Jin Date: Fri, 31 May 2024 18:04:14 -0700 Subject: [PATCH 533/662] feat: Adding support of client authentication method. (#1814) * feat: support extra parameter of client authentication method * fix lint * fix lint * fix lint * fix lint * Update src/auth/oauth2client.ts Co-authored-by: aeitzman <12433791+aeitzman@users.noreply.github.com> * Update src/auth/oauth2client.ts Co-authored-by: aeitzman <12433791+aeitzman@users.noreply.github.com> * fix the 'any' type into strict check * fix lint * fix lint * Update src/auth/oauth2client.ts Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * Update src/auth/oauth2client.ts Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * Update src/auth/oauth2client.ts Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * Update src/auth/oauth2client.ts Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * Update src/auth/oauth2client.ts Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * Update src/auth/oauth2client.ts Co-authored-by: aeitzman <12433791+aeitzman@users.noreply.github.com> * address comments * fix tests * fix tests * fix tests and lint * Update src/auth/oauth2client.ts Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> * addressing comments * adding validation of no auth header * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead * Update test/test.oauth2.ts Co-authored-by: Daniel Bankhead * Update test/test.oauth2.ts Co-authored-by: Daniel Bankhead * Update test/test.oauth2.ts Co-authored-by: Daniel Bankhead * fix CI after apply changes * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead * fix syntax of post value * fix lint * fix the client_secret field * refactor: interface and readability --------- Co-authored-by: aeitzman <12433791+aeitzman@users.noreply.github.com> Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com> Co-authored-by: Daniel Bankhead Co-authored-by: Daniel Bankhead --- src/auth/oauth2client.ts | 54 ++++++++++++++++++++--- src/index.ts | 1 + test/test.oauth2.ts | 94 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 142 insertions(+), 7 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index ce1fb829..4c60c5f9 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -68,6 +68,16 @@ export enum CertificateFormat { JWK = 'JWK', } +/** + * The client authentication type. Supported values are basic, post, and none. + * https://datatracker.ietf.org/doc/html/rfc7591#section-2 + */ +export enum ClientAuthentication { + ClientSecretPost = 'ClientSecretPost', + ClientSecretBasic = 'ClientSecretBasic', + None = 'None', +} + export interface GetTokenOptions { code: string; codeVerifier?: string; @@ -86,6 +96,19 @@ export interface GetTokenOptions { redirect_uri?: string; } +/** + * An interface for preparing {@link GetTokenOptions} as a querystring. + */ +interface GetTokenQuery { + client_id?: string; + client_secret?: string; + code_verifier?: string; + code: string; + grant_type: 'authorization_code'; + redirect_uri?: string; + [key: string]: string | undefined; +} + export interface TokenInfo { /** * The application that is the intended user of the access token. @@ -475,6 +498,12 @@ export interface OAuth2ClientOptions extends AuthClientOptions { * The allowed OAuth2 token issuers. */ issuers?: string[]; + /** + * The client authentication type. Supported values are basic, post, and none. + * Defaults to post if not provided. + * https://datatracker.ietf.org/doc/html/rfc7591#section-2 + */ + clientAuthentication?: ClientAuthentication; } // Re-exporting here for backwards compatibility @@ -491,6 +520,7 @@ export class OAuth2Client extends AuthClient { protected refreshTokenPromises = new Map>(); readonly endpoints: Readonly; readonly issuers: string[]; + readonly clientAuthentication: ClientAuthentication; // TODO: refactor tests to make this private _clientId?: string; @@ -542,6 +572,8 @@ export class OAuth2Client extends AuthClient { oauth2IapPublicKeyUrl: 'https://www.gstatic.com/iap/verify/public_key', ...opts.endpoints, }; + this.clientAuthentication = + opts.clientAuthentication || ClientAuthentication.ClientSecretPost; this.issuers = opts.issuers || [ 'accounts.google.com', @@ -660,20 +692,30 @@ export class OAuth2Client extends AuthClient { options: GetTokenOptions ): Promise { const url = this.endpoints.oauth2TokenUrl.toString(); - const values = { - code: options.code, + const headers: Headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + const values: GetTokenQuery = { client_id: options.client_id || this._clientId, - client_secret: this._clientSecret, - redirect_uri: options.redirect_uri || this.redirectUri, - grant_type: 'authorization_code', code_verifier: options.codeVerifier, + code: options.code, + grant_type: 'authorization_code', + redirect_uri: options.redirect_uri || this.redirectUri, }; + if (this.clientAuthentication === ClientAuthentication.ClientSecretBasic) { + const basic = Buffer.from(`${this._clientId}:${this._clientSecret}`); + + headers['Authorization'] = `Basic ${basic.toString('base64')}`; + } + if (this.clientAuthentication === ClientAuthentication.ClientSecretPost) { + values.client_secret = this._clientSecret; + } const res = await this.transporter.request({ ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, data: querystring.stringify(values), - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + headers, }); const tokens = res.data as Credentials; if (res.data && res.data.expires_in) { diff --git a/src/index.ts b/src/index.ts index 1ab32948..fe3b69eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,7 @@ export { RefreshOptions, TokenInfo, VerifyIdTokenOptions, + ClientAuthentication, } from './auth/oauth2client'; export {LoginTicket, TokenPayload} from './auth/loginticket'; export { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index b836656f..ebd42ef0 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -23,7 +23,12 @@ import * as path from 'path'; import * as qs from 'querystring'; import * as sinon from 'sinon'; -import {CodeChallengeMethod, Credentials, OAuth2Client} from '../src'; +import { + CodeChallengeMethod, + Credentials, + OAuth2Client, + ClientAuthentication, +} from '../src'; import {LoginTicket} from '../src/auth/loginticket'; nock.disableNetConnect(); @@ -1366,6 +1371,7 @@ describe('oauth2', () => { reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, }) .post('/token') + .matchHeader('authorization', value => value === undefined) .reply(200, { access_token: 'abc', refresh_token: '123', @@ -1421,6 +1427,92 @@ describe('oauth2', () => { assert.strictEqual(params.client_id, 'overridden'); }); + it('getToken should use basic header auth if provided in options', async () => { + const authurl = 'https://sts.googleapis.com/v1/'; + const basic_auth = + 'Basic ' + + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'); + const scope = nock(authurl) + .post('/oauthtoken') + .matchHeader('Authorization', basic_auth) + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + const opts = { + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + redirectUri: REDIRECT_URI, + endpoints: { + oauth2AuthBaseUrl: 'https://auth.cloud.google/authorize', + oauth2TokenUrl: 'https://sts.googleapis.com/v1/oauthtoken', + tokenInfoUrl: 'https://sts.googleapis.com/v1/introspect', + }, + clientAuthentication: ClientAuthentication.ClientSecretBasic, + }; + const oauth2client = new OAuth2Client(opts); + const res = await oauth2client.getToken({ + code: 'code here', + client_id: CLIENT_ID, + }); + scope.done(); + assert(res.res); + assert.equal(res.res.data.access_token, 'abc'); + }); + + it('getToken should not use basic header auth if provided none in options and fail', async () => { + const authurl = 'https://some.example.auth/'; + const scope = nock(authurl) + .post('/token') + .matchHeader('Authorization', val => val === undefined) + .reply(401); + const opts = { + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + redirectUri: REDIRECT_URI, + endpoints: { + oauth2AuthBaseUrl: 'https://auth.cloud.google/authorize', + oauth2TokenUrl: 'https://some.example.auth/token', + }, + clientAuthentication: ClientAuthentication.None, + }; + const oauth2client = new OAuth2Client(opts); + assert.equal( + oauth2client.clientAuthentication, + ClientAuthentication.None + ); + + try { + await oauth2client.getToken({ + code: 'code here', + client_id: CLIENT_ID, + }); + throw new Error('Expected an error'); + } catch (err) { + assert(err instanceof GaxiosError); + assert.equal(err.response?.status, 401); + } finally { + scope.done(); + } + }); + + it('getToken should use auth secret post if not provided in options', async () => { + const opts = { + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + redirectUri: REDIRECT_URI, + endpoints: { + oauth2TokenUrl: 'mytokenurl', + }, + }; + const oauth2client = new OAuth2Client(opts); + assert.equal( + oauth2client.clientAuthentication, + ClientAuthentication.ClientSecretPost + ); + }); + it('should return expiry_date', done => { const now = new Date().getTime(); const scope = nock(baseUrl, { From 0d0bc2a67e18f4f325200e3ad55c03ecbb32055d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 09:37:56 -0700 Subject: [PATCH 534/662] chore(main): release 9.11.0 (#1821) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e81627c..995e2269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.11.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.10.0...v9.11.0) (2024-06-01) + + +### Features + +* Adding support of client authentication method. ([#1814](https://github.com/googleapis/google-auth-library-nodejs/issues/1814)) ([4a14e8c](https://github.com/googleapis/google-auth-library-nodejs/commit/4a14e8c3bdcfa9d8531a231b00b946728530ce12)) + ## [9.10.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.9.0...v9.10.0) (2024-05-10) diff --git a/package.json b/package.json index 0588d0f0..5ce9ddd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.10.0", + "version": "9.11.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index d0684f68..7ca47f8b 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^18.0.0", - "google-auth-library": "^9.10.0", + "google-auth-library": "^9.11.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From ca8c1f0146697322ab72308a5253628e2a9d9dd0 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 12 Jun 2024 14:22:34 -0700 Subject: [PATCH 535/662] chore: Fix lint issues (#1826) * chore: Fix lint issues * chore: fix copyright --- browser-test/test.oauth2.ts | 3 --- samples/oauth2-codeVerifier.js | 6 +++++- samples/oauth2.js | 6 +++++- samples/puppeteer/oauth2-test.js | 13 ++++++++++--- samples/verifyIdToken.js | 6 +++++- src/crypto/browser/crypto.ts | 9 ++------- 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/browser-test/test.oauth2.ts b/browser-test/test.oauth2.ts index 5626575d..e7b9cb9a 100644 --- a/browser-test/test.oauth2.ts +++ b/browser-test/test.oauth2.ts @@ -160,10 +160,8 @@ describe('Browser OAuth2 tests', () => { '}'; const envelope = JSON.stringify({kid: 'keyid', alg: 'RS256'}); let data = - // eslint-disable-next-line node/no-unsupported-features/node-builtins base64js.fromByteArray(new TextEncoder().encode(envelope)) + '.' + - // eslint-disable-next-line node/no-unsupported-features/node-builtins base64js.fromByteArray(new TextEncoder().encode(idToken)); const algo = { name: 'RSASSA-PKCS1-v1_5', @@ -181,7 +179,6 @@ describe('Browser OAuth2 tests', () => { const signature = await window.crypto.subtle.sign( algo, cryptoKey, - // eslint-disable-next-line node/no-unsupported-features/node-builtins new TextEncoder().encode(data) ); data += '.' + base64js.fromByteArray(new Uint8Array(signature)); diff --git a/samples/oauth2-codeVerifier.js b/samples/oauth2-codeVerifier.js index d91b7fa1..37065042 100644 --- a/samples/oauth2-codeVerifier.js +++ b/samples/oauth2-codeVerifier.js @@ -20,7 +20,11 @@ const open = require('open'); const destroyer = require('server-destroy'); // Download your OAuth2 configuration from the Google Developer Console. -const keys = require('./oauth2.keys.json'); +/** + * @example + * require('./oauth2.keys.json'); + */ +const keys = {}; /** * Start by acquiring a pre-authenticated oAuth2 client. diff --git a/samples/oauth2.js b/samples/oauth2.js index 5d177a5d..a10fab2b 100644 --- a/samples/oauth2.js +++ b/samples/oauth2.js @@ -20,7 +20,11 @@ const open = require('open'); const destroyer = require('server-destroy'); // Download your OAuth2 configuration from the Google -const keys = require('./oauth2.keys.json'); +/** + * @example + * require('./oauth2.keys.json'); + */ +const keys = {}; /** * Start by acquiring a pre-authenticated oAuth2 client. diff --git a/samples/puppeteer/oauth2-test.js b/samples/puppeteer/oauth2-test.js index 0d4d6228..71fa5908 100644 --- a/samples/puppeteer/oauth2-test.js +++ b/samples/puppeteer/oauth2-test.js @@ -1,4 +1,4 @@ -// Copyright 2018, Google, LLC +// Copyright 2018 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 @@ -22,7 +22,11 @@ const puppeteer = require('puppeteer'); const url = require('url'); const http = require('http'); -const keys = require('../oauth2.keys.json'); +/** + * @example + * require('../oauth2.keys.json'); + */ +const keys = {}; /** * Keep a config.keys.json with a username and password @@ -31,8 +35,11 @@ const keys = require('../oauth2.keys.json'); * "username": "your-user-name@gmail.com", * "password": "your-password" * } + * + * @example + * require('../config.keys.json'); */ -const config = require('../config.keys.json'); +const config = {}; async function main() { const oAuth2Client = await getAuthenticatedClient(); diff --git a/samples/verifyIdToken.js b/samples/verifyIdToken.js index 89269892..bb9f0dc1 100644 --- a/samples/verifyIdToken.js +++ b/samples/verifyIdToken.js @@ -20,7 +20,11 @@ const open = require('open'); const destroyer = require('server-destroy'); // Download your OAuth2 configuration from the Google -const keys = require('./oauth2.keys.json'); +/** + * @example + * require('./oauth2.keys.json'); + */ +const keys = {}; /** * Start by acquiring a pre-authenticated oAuth2 client. diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index e46096f7..df584ef1 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -39,7 +39,6 @@ export class BrowserCrypto implements Crypto { // To calculate SHA256 digest using SubtleCrypto, we first // need to convert an input string to an ArrayBuffer: - // eslint-disable-next-line node/no-unsupported-features/node-builtins const inputBuffer = new TextEncoder().encode(str); // Result is ArrayBuffer as well. @@ -74,7 +73,7 @@ export class BrowserCrypto implements Crypto { name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'}, }; - // eslint-disable-next-line node/no-unsupported-features/node-builtins + const dataArray = new TextEncoder().encode(data); const signatureArray = base64js.toByteArray( BrowserCrypto.padBase64(signature) @@ -103,7 +102,7 @@ export class BrowserCrypto implements Crypto { name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'}, }; - // eslint-disable-next-line node/no-unsupported-features/node-builtins + const dataArray = new TextEncoder().encode(data); const cryptoKey = await window.crypto.subtle.importKey( 'jwk', @@ -121,13 +120,11 @@ export class BrowserCrypto implements Crypto { decodeBase64StringUtf8(base64: string): string { const uint8array = base64js.toByteArray(BrowserCrypto.padBase64(base64)); - // eslint-disable-next-line node/no-unsupported-features/node-builtins const result = new TextDecoder().decode(uint8array); return result; } encodeBase64StringUtf8(text: string): string { - // eslint-disable-next-line node/no-unsupported-features/node-builtins const uint8array = new TextEncoder().encode(text); const result = base64js.fromByteArray(uint8array); return result; @@ -145,7 +142,6 @@ export class BrowserCrypto implements Crypto { // To calculate SHA256 digest using SubtleCrypto, we first // need to convert an input string to an ArrayBuffer: - // eslint-disable-next-line node/no-unsupported-features/node-builtins const inputBuffer = new TextEncoder().encode(str); // Result is ArrayBuffer as well. @@ -175,7 +171,6 @@ export class BrowserCrypto implements Crypto { ? key : String.fromCharCode(...new Uint16Array(key)); - // eslint-disable-next-line node/no-unsupported-features/node-builtins const enc = new TextEncoder(); const cryptoKey = await window.crypto.subtle.importKey( 'raw', From b4bd8dc05f5ce672695e65c3ec48a5926ac39f2f Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:56:47 -0700 Subject: [PATCH 536/662] ci: Enable `constraintsFiltering` for Node.js Libraries (#1825) chore: Enable `constraintsFiltering` for Node.js Libraries Source-Link: https://github.com/googleapis/synthtool/commit/dae1282201b64e4da3ad512632cb4dda70a832a1 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:d920257482ca1cd72978f29f7d28765a9f8c758c21ee0708234db5cf4c5016c2 Co-authored-by: Owl Bot Co-authored-by: Daniel Bankhead --- .github/.OwlBot.lock.yaml | 4 ++-- renovate.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 34bb2086..9e90d54b 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:68e1cece0d6d3336c4f1cb9d2857b020af5574dff6da6349293d1c6d4eea82d8 -# created: 2024-05-31T15:46:42.989947733Z + digest: sha256:d920257482ca1cd72978f29f7d28765a9f8c758c21ee0708234db5cf4c5016c2 +# created: 2024-06-12T16:18:41.688792375Z diff --git a/renovate.json b/renovate.json index 26428fcf..c5c702cf 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,7 @@ "docker:disable", ":disableDependencyDashboard" ], + "constraintsFiltering": "strict", "pinVersions": false, "rebaseStalePrs": true, "schedule": [ From b070ffbfeb35a7f4552e86bf1840645096951b58 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 13 Jun 2024 02:58:52 +0200 Subject: [PATCH 537/662] fix(deps): update dependency @googleapis/iam to v19 (#1823) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 7ca47f8b..1895dfd8 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^18.0.0", + "@googleapis/iam": "^19.0.0", "google-auth-library": "^9.11.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From e31a831417692e730f79d42608bd543046070ae3 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 1 Jul 2024 23:18:27 +0200 Subject: [PATCH 538/662] fix(deps): update dependency @googleapis/iam to v20 (#1832) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 1895dfd8..99148b2f 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^19.0.0", + "@googleapis/iam": "^20.0.0", "google-auth-library": "^9.11.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From 0b78d91534d97b37859a2303b332f8ccd52dbf69 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 12 Jul 2024 00:16:10 +0200 Subject: [PATCH 539/662] chore(deps): update actions/checkout digest to 692973e (#1828) Co-authored-by: Daniel Bankhead --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 23f2b874..f761a6ee 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [14, 16, 18, 20] steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@v4 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@v4 with: node-version: 14 From 5745a49df31ff87c0e53edf44671f3a10c024d9f Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 26 Jul 2024 12:33:14 -0700 Subject: [PATCH 540/662] feat: Expose More Public API Types (#1838) --- src/auth/baseexternalclient.ts | 7 ++++++- src/index.ts | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 3513e7e2..bd234ec3 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -186,7 +186,12 @@ export interface BaseExternalAccountClientOptions */ export interface IamGenerateAccessTokenResponse { accessToken: string; - // ISO format used for expiration time: 2014-10-02T15:01:23.045123456Z + /** + * ISO format used for expiration time. + * + * @example + * '2014-10-02T15:01:23.045123456Z' + */ expireTime: string; } diff --git a/src/index.ts b/src/index.ts index fe3b69eb..3228339d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,10 +51,19 @@ export { UserRefreshClient, UserRefreshClientOptions, } from './auth/refreshclient'; -export {AwsClient, AwsClientOptions} from './auth/awsclient'; +export { + AwsClient, + AwsClientOptions, + AwsSecurityCredentialsSupplier, +} from './auth/awsclient'; +export { + AwsSecurityCredentials, + AwsRequestSigner, +} from './auth/awsrequestsigner'; export { IdentityPoolClient, IdentityPoolClientOptions, + SubjectTokenSupplier, } from './auth/identitypoolclient'; export { ExternalAccountClient, @@ -63,6 +72,9 @@ export { export { BaseExternalAccountClient, BaseExternalAccountClientOptions, + SharedExternalAccountClientOptions, + ExternalAccountSupplierContext, + IamGenerateAccessTokenResponse, } from './auth/baseexternalclient'; export { CredentialAccessBoundary, @@ -71,6 +83,7 @@ export { export { PluggableAuthClient, PluggableAuthClientOptions, + ExecutableError, } from './auth/pluggable-auth-client'; export {PassThroughClient} from './auth/passthrough'; export {DefaultTransporter} from './transporters'; From 76666f82a083163351256e48468cf9139d4d4b2e Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:41:03 -0700 Subject: [PATCH 541/662] chore(main): release 9.12.0 (#1827) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 995e2269..6d37148c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.12.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.11.0...v9.12.0) (2024-07-26) + + +### Features + +* Expose More Public API Types ([#1838](https://github.com/googleapis/google-auth-library-nodejs/issues/1838)) ([5745a49](https://github.com/googleapis/google-auth-library-nodejs/commit/5745a49df31ff87c0e53edf44671f3a10c024d9f)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v19 ([#1823](https://github.com/googleapis/google-auth-library-nodejs/issues/1823)) ([b070ffb](https://github.com/googleapis/google-auth-library-nodejs/commit/b070ffbfeb35a7f4552e86bf1840645096951b58)) +* **deps:** Update dependency @googleapis/iam to v20 ([#1832](https://github.com/googleapis/google-auth-library-nodejs/issues/1832)) ([e31a831](https://github.com/googleapis/google-auth-library-nodejs/commit/e31a831417692e730f79d42608bd543046070ae3)) + ## [9.11.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.10.0...v9.11.0) (2024-06-01) diff --git a/package.json b/package.json index 5ce9ddd9..8a71b1a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.11.0", + "version": "9.12.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 99148b2f..3cb0be2c 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^20.0.0", - "google-auth-library": "^9.11.0", + "google-auth-library": "^9.12.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From 0e08fc58eb61bba431ab4f217f7f7ad3a7dce9df Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 29 Jul 2024 13:20:34 -0700 Subject: [PATCH 542/662] feat: Group Concurrent Access Token Requests for Base External Clients (#1840) --- src/auth/baseexternalclient.ts | 18 ++++++++++ test/test.baseexternalclient.ts | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index bd234ec3..26436979 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -252,6 +252,11 @@ export abstract class BaseExternalAccountClient extends AuthClient { */ protected cloudResourceManagerURL: URL | string; protected supplierContext: ExternalAccountSupplierContext; + /** + * A pending access token request. Used for concurrent calls. + */ + #pendingAccessToken: Promise | null = null; + /** * Instantiate a BaseExternalAccountClient instance using the provided JSON * object loaded from an external account credentials file. @@ -545,6 +550,19 @@ export abstract class BaseExternalAccountClient extends AuthClient { * @return A promise that resolves with the fresh GCP access tokens. */ protected async refreshAccessTokenAsync(): Promise { + // Use an existing access token request, or cache a new one + this.#pendingAccessToken = + this.#pendingAccessToken || this.#internalRefreshAccessTokenAsync(); + + try { + return await this.#pendingAccessToken; + } finally { + // clear pending access token for future requests + this.#pendingAccessToken = null; + } + } + + async #internalRefreshAccessTokenAsync(): Promise { // Retrieve the external credential. const subjectToken = await this.retrieveSubjectToken(); // Construct the STS credentials options. diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index a0974a85..71c4ec7b 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -326,6 +326,70 @@ describe('BaseExternalAccountClient', () => { scope.done(); }); + + it('should not duplicate access token requests for concurrent requests', async () => { + const client = new TestExternalAccountClient(externalAccountOptionsNoUrl); + const RESPONSE_A = { + access_token: 'ACCESS_TOKEN', + issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', + token_type: 'Bearer', + expires_in: ONE_HOUR_IN_SECS, + scope: 'scope1 scope2', + }; + + const RESPONSE_B = { + ...RESPONSE_A, + access_token: 'ACCESS_TOKEN_2', + }; + + const scope = mockStsTokenExchange([ + { + statusCode: 200, + response: RESPONSE_A, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + { + statusCode: 200, + response: RESPONSE_B, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_1', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]); + + // simulate 5 concurrent requests + const calls = [ + client.getAccessToken(), + client.getAccessToken(), + client.getAccessToken(), + client.getAccessToken(), + client.getAccessToken(), + ]; + + for (const {token} of await Promise.all(calls)) { + assert.strictEqual(token, RESPONSE_A.access_token); + } + + // this should be handled in a second request as the above were all awaited and we're forcing an expiration + client.eagerRefreshThresholdMillis = RESPONSE_A.expires_in * 1000; + assert((await client.getAccessToken()).token, RESPONSE_B.access_token); + + scope.done(); + }); }); describe('projectNumber', () => { From 8c82467f541ae6585e0bcc74a7fc866a22d7c5f1 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:47:19 -0700 Subject: [PATCH 543/662] chore(main): release 9.13.0 (#1841) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d37148c..d19b896e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.13.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.12.0...v9.13.0) (2024-07-29) + + +### Features + +* Group Concurrent Access Token Requests for Base External Clients ([#1840](https://github.com/googleapis/google-auth-library-nodejs/issues/1840)) ([0e08fc5](https://github.com/googleapis/google-auth-library-nodejs/commit/0e08fc58eb61bba431ab4f217f7f7ad3a7dce9df)) + ## [9.12.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.11.0...v9.12.0) (2024-07-26) diff --git a/package.json b/package.json index 8a71b1a2..d6be2da4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.12.0", + "version": "9.13.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 3cb0be2c..4733d8c4 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^20.0.0", - "google-auth-library": "^9.12.0", + "google-auth-library": "^9.13.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From 3ae120d0a45c95e36c59c9ac8286483938781f30 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Wed, 14 Aug 2024 06:46:32 -0700 Subject: [PATCH 544/662] feat: Add `AnyAuthClient` type (#1843) * feat: Add `AuthClients` type * refactor: readability * docs: docs --- src/auth/googleauth.ts | 7 ++++--- src/index.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 40b71d2f..b5cf27b1 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -54,6 +54,7 @@ import { ExternalAccountAuthorizedUserClientOptions, } from './externalAccountAuthorizedUserClient'; import {originalOrCamelOptions} from '../util'; +import {AnyAuthClient} from '..'; /** * Defines all types of explicit clients that are determined via ADC JSON @@ -162,7 +163,7 @@ export class GoogleAuth { useJWTAccessWithScope?: boolean; defaultServicePath?: string; - // Note: this properly is only public to satisify unit tests. + // Note: this properly is only public to satisfy unit tests. // https://github.com/Microsoft/TypeScript/issues/5228 get isGCE() { return this.checkIsGCE; @@ -174,7 +175,7 @@ export class GoogleAuth { // To save the contents of the JSON credential file jsonContent: JWTInput | ExternalAccountClientOptions | null = null; - cachedCredential: JSONClient | Impersonated | Compute | T | null = null; + cachedCredential: AnyAuthClient | T | null = null; /** * Scopes populated by the client library by default. We differentiate between @@ -457,7 +458,7 @@ export class GoogleAuth { } private async prepareAndCacheADC( - credential: JSONClient | Impersonated | Compute | T, + credential: AnyAuthClient, quotaProjectIdOverride?: string ): Promise { const projectId = await this.getProjectIdOptional(); diff --git a/src/index.ts b/src/index.ts index 3228339d..6652a7b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import {GoogleAuth} from './auth/googleauth'; export * as gcpMetadata from 'gcp-metadata'; export * as gaxios from 'gaxios'; +import {AuthClient} from './auth/authclient'; export {AuthClient, DEFAULT_UNIVERSE} from './auth/authclient'; export {Compute, ComputeOptions} from './auth/computeclient'; export { @@ -88,5 +89,15 @@ export { export {PassThroughClient} from './auth/passthrough'; export {DefaultTransporter} from './transporters'; +type ALL_EXPORTS = (typeof import('./'))[keyof typeof import('./')]; + +/** + * A union type for all {@link AuthClient `AuthClient`}s. + */ +export type AnyAuthClient = InstanceType< + // Extract All `AuthClient`s from exports + Extract +>; + const auth = new GoogleAuth(); export {auth, GoogleAuth}; From 8f45561647f15a2b8fbbd5020128b314a93ef27e Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 16 Aug 2024 12:52:55 -0700 Subject: [PATCH 545/662] test: Remove webpack max size requirement (#1844) * test: Remove webpack max size requirement * chore: pin `cheerio` of `@compodoc/compodoc` --- package.json | 1 + system-test/test.kitchen.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d6be2da4..c5124376 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "assert-rejects": "^1.0.0", "c8": "^8.0.0", "chai": "^4.2.0", + "cheerio": "1.0.0-rc.12", "codecov": "^3.0.2", "execa": "^5.0.0", "gts": "^5.0.0", diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 8e1e89f6..e818ef26 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -63,8 +63,8 @@ describe('pack and install', () => { // we expect npm install is executed in the before hook await execa('npx', ['webpack'], {cwd: `${stagingDir}/`, stdio: 'inherit'}); const bundle = path.join(stagingDir, 'dist', 'bundle.min.js'); - const stat = fs.statSync(bundle); - assert(stat.size < 512 * 1024); + // ensure it is a non-empty bundle + assert(fs.statSync(bundle).size); }); /** From e9459f3d11418ce8afd4fe87cd92d4b2d06457ba Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 19 Aug 2024 19:31:31 +0200 Subject: [PATCH 546/662] fix(deps): update dependency @googleapis/iam to v21 (#1847) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 4733d8c4..753884e2 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,7 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^20.0.0", + "@googleapis/iam": "^21.0.0", "google-auth-library": "^9.13.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From 5fc3bccacc74082f71983595dd7654b1b60be0f8 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 19 Aug 2024 13:14:17 -0700 Subject: [PATCH 547/662] feat: Extend API Key Support (#1835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Extend API Key Support * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Support `apiKey` as an ADC fallback * refactor: Move `apiKey` to base client options * docs: clarity * refactor: API Key Support * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: type * feat: Export Error Messages * test: Add tests for API Key Support * test: cleanup * docs: Clarifications * refactor: streamline * chore: merge cleanup * docs(sample): Add API Key Sample * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: OCD * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Apply suggestions from code review Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- .readme-partials.yaml | 22 +++++++++ README.md | 23 +++++++++ samples/README.md | 18 +++++++ samples/authenticateAPIKey.js | 63 ++++++++++++++++++++++++ samples/package.json | 1 + src/auth/authclient.ts | 6 +++ src/auth/googleauth.ts | 93 ++++++++++++++++++++--------------- src/auth/oauth2client.ts | 2 - test/test.googleauth.ts | 37 ++++++++++---- test/test.oauth2.ts | 7 +++ 10 files changed, 222 insertions(+), 50 deletions(-) create mode 100644 samples/authenticateAPIKey.js diff --git a/.readme-partials.yaml b/.readme-partials.yaml index cce6a02f..7b26d0d2 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -220,6 +220,28 @@ body: |- This method will throw if the token is invalid. + #### Using an API Key + + An API key can be provided to the constructor: + ```js + const client = new OAuth2Client({ + apiKey: 'my-api-key' + }); + ``` + + Note, classes that extend from this can utilize this parameter as well, such as `JWT` and `UserRefreshClient`. + + Additionally, an API key can be used in `GoogleAuth` via the `clientOptions` parameter and will be passed to any generated `OAuth2Client` instances: + ```js + const auth = new GoogleAuth({ + clientOptions: { + apiKey: 'my-api-key' + } + }) + ``` + + API Key support varies by API. + ## JSON Web Tokens The Google Developers Console provides a `.json` file that you can use to configure a JWT auth client and authenticate your requests, for example when using a service account. diff --git a/README.md b/README.md index 55d86876..e15d28fc 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,28 @@ console.log(tokenInfo.scopes); This method will throw if the token is invalid. +#### Using an API Key + +An API key can be provided to the constructor: +```js +const client = new OAuth2Client({ + apiKey: 'my-api-key' +}); +``` + +Note, classes that extend from this can utilize this parameter as well, such as `JWT` and `UserRefreshClient`. + +Additionally, an API key can be used in `GoogleAuth` via the `clientOptions` parameter and will be passed to any generated `OAuth2Client` instances: +```js +const auth = new GoogleAuth({ + clientOptions: { + apiKey: 'my-api-key' + } +}) +``` + +API Key support varies by API. + ## JSON Web Tokens The Google Developers Console provides a `.json` file that you can use to configure a JWT auth client and authenticate your requests, for example when using a service account. @@ -1326,6 +1348,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar | Sample | Source Code | Try it | | --------------------------- | --------------------------------- | ------ | | Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/adc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/adc.js,samples/README.md) | +| Authenticate API Key | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateAPIKey.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateAPIKey.js,samples/README.md) | | Authenticate Explicit | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateExplicit.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateExplicit.js,samples/README.md) | | Authenticate Implicit With Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateImplicitWithAdc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateImplicitWithAdc.js,samples/README.md) | | Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) | diff --git a/samples/README.md b/samples/README.md index 115dba68..5ea1e11e 100644 --- a/samples/README.md +++ b/samples/README.md @@ -13,6 +13,7 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra * [Before you begin](#before-you-begin) * [Samples](#samples) * [Adc](#adc) + * [Authenticate API Key](#authenticate-api-key) * [Authenticate Explicit](#authenticate-explicit) * [Authenticate Implicit With Adc](#authenticate-implicit-with-adc) * [Compute](#compute) @@ -67,6 +68,23 @@ __Usage:__ +### Authenticate API Key + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateAPIKey.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateAPIKey.js,samples/README.md) + +__Usage:__ + + +`node samples/authenticateAPIKey.js` + + +----- + + + + ### Authenticate Explicit View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateExplicit.js). diff --git a/samples/authenticateAPIKey.js b/samples/authenticateAPIKey.js new file mode 100644 index 00000000..da9df2c1 --- /dev/null +++ b/samples/authenticateAPIKey.js @@ -0,0 +1,63 @@ +// Copyright 2024 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. + +/** + * Lists storage buckets by authenticating with ADC. + */ +function main() { + // [START apikeys_authenticate_api_key] + + const { + v1: {LanguageServiceClient}, + } = require('@google-cloud/language'); + + /** + * Authenticates with an API key for Google Language service. + * + * @param {string} apiKey An API Key to use + */ + async function authenticateWithAPIKey(apiKey) { + const language = new LanguageServiceClient({apiKey}); + + // Alternatively: + // const auth = new GoogleAuth({apiKey}); + // const {GoogleAuth} = require('google-auth-library'); + // const language = new LanguageServiceClient({auth}); + + const text = 'Hello, world!'; + + const [response] = await language.analyzeSentiment({ + document: { + content: text, + type: 'PLAIN_TEXT', + }, + }); + + console.log(`Text: ${text}`); + console.log( + `Sentiment: ${response.documentSentiment.score}, ${response.documentSentiment.magnitude}` + ); + console.log('Successfully authenticated using the API key'); + } + + authenticateWithAPIKey(); + // [END apikeys_authenticate_api_key] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); + +main(...process.argv.slice(2)); diff --git a/samples/package.json b/samples/package.json index 753884e2..be6ae7a1 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,6 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@google-cloud/language": "^6.5.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^21.0.0", "google-auth-library": "^9.13.0", diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 69301db8..2f2b384c 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -72,6 +72,10 @@ interface AuthJSONOptions { */ export interface AuthClientOptions extends Partial> { + /** + * An API key to use, optional. + */ + apiKey?: string; credentials?: Credentials; /** @@ -170,6 +174,7 @@ export abstract class AuthClient extends EventEmitter implements CredentialsClient { + apiKey?: string; projectId?: string | null; /** * The quota project ID. The quota project can be used by client libraries for the billing purpose. @@ -188,6 +193,7 @@ export abstract class AuthClient const options = originalOrCamelOptions(opts); // Shared auth options + this.apiKey = opts.apiKey; this.projectId = options.get('project_id') ?? null; this.quotaProjectId = options.get('quota_project_id'); this.credentials = options.get('credentials') ?? {}; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index b5cf27b1..06ae466b 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -85,6 +85,11 @@ export interface ADCResponse { } export interface GoogleAuthOptions { + /** + * An API key to use, optional. Cannot be used with {@link GoogleAuthOptions.credentials `credentials`}. + */ + apiKey?: string; + /** * An `AuthClient` to use */ @@ -102,6 +107,7 @@ export interface GoogleAuthOptions { /** * Object containing client_email and private_key properties, or the * external account client options. + * Cannot be used with {@link GoogleAuthOptions.apiKey `apiKey`}. */ credentials?: JWTInput | ExternalAccountClientOptions; @@ -136,7 +142,9 @@ export interface GoogleAuthOptions { export const CLOUD_SDK_CLIENT_ID = '764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com'; -const GoogleAuthExceptionMessages = { +export const GoogleAuthExceptionMessages = { + API_KEY_WITH_CREDENTIALS: + 'API Keys and Credentials are mutually exclusive authentication methods and cannot be used together.', NO_PROJECT_ID_FOUND: 'Unable to detect a Project Id in the current environment. \n' + 'To learn more about authentication and Google APIs, visit: \n' + @@ -145,6 +153,8 @@ const GoogleAuthExceptionMessages = { 'Unable to find credentials in current environment. \n' + 'To learn more about authentication and Google APIs, visit: \n' + 'https://cloud.google.com/docs/authentication/getting-started', + NO_ADC_FOUND: + 'Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.', NO_UNIVERSE_DOMAIN_FOUND: 'Unable to detect a Universe Domain in the current environment.\n' + 'To learn more about Universe Domain retrieval, visit: \n' + @@ -174,6 +184,7 @@ export class GoogleAuth { // To save the contents of the JSON credential file jsonContent: JWTInput | ExternalAccountClientOptions | null = null; + apiKey: string | null; cachedCredential: AnyAuthClient | T | null = null; @@ -202,15 +213,21 @@ export class GoogleAuth { * * @param opts */ - constructor(opts?: GoogleAuthOptions) { - opts = opts || {}; - + constructor(opts: GoogleAuthOptions = {}) { 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 || {}; + this.jsonContent = opts.credentials || null; + this.apiKey = opts.apiKey || this.clientOptions.apiKey || null; + + // Cannot use both API Key + Credentials + if (this.apiKey && (this.jsonContent || this.clientOptions.credentials)) { + throw new RangeError( + GoogleAuthExceptionMessages.API_KEY_WITH_CREDENTIALS + ); + } if (opts.universeDomain) { this.clientOptions.universeDomain = opts.universeDomain; @@ -402,13 +419,10 @@ export class GoogleAuth { // This will also preserve one's configured quota project, in case they // set one directly on the credential previously. if (this.cachedCredential) { - return await this.prepareAndCacheADC(this.cachedCredential); + // cache, while preserving existing quota project preferences + return await this.#prepareAndCacheClient(this.cachedCredential, null); } - // Since this is a 'new' ADC to cache we will use the environment variable - // if it's available. We prefer this value over the value from ADC. - const quotaProjectIdOverride = process.env['GOOGLE_CLOUD_QUOTA_PROJECT']; - let credential: JSONClient | null; // Check for the existence of a local environment variable pointing to the // location of the credential file. This is typically used in local @@ -422,7 +436,7 @@ export class GoogleAuth { credential.scopes = this.getAnyScopes(); } - return await this.prepareAndCacheADC(credential, quotaProjectIdOverride); + return await this.#prepareAndCacheClient(credential); } // Look in the well-known credential file location. @@ -434,7 +448,7 @@ export class GoogleAuth { } else if (credential instanceof BaseExternalAccountClient) { credential.scopes = this.getAnyScopes(); } - return await this.prepareAndCacheADC(credential, quotaProjectIdOverride); + return await this.#prepareAndCacheClient(credential); } // Determine if we're running on GCE. @@ -446,20 +460,15 @@ export class GoogleAuth { } (options as ComputeOptions).scopes = this.getAnyScopes(); - return await this.prepareAndCacheADC( - new Compute(options), - quotaProjectIdOverride - ); + return await this.#prepareAndCacheClient(new Compute(options)); } - throw new Error( - 'Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.' - ); + throw new Error(GoogleAuthExceptionMessages.NO_ADC_FOUND); } - private async prepareAndCacheADC( - credential: AnyAuthClient, - quotaProjectIdOverride?: string + async #prepareAndCacheClient( + credential: AnyAuthClient | T, + quotaProjectIdOverride = process.env['GOOGLE_CLOUD_QUOTA_PROJECT'] || null ): Promise { const projectId = await this.getProjectIdOptional(); @@ -806,15 +815,14 @@ export class GoogleAuth { /** * Create a credentials instance using the given API key string. + * The created client is not cached. In order to create and cache it use the {@link GoogleAuth.getClient `getClient`} method after first providing an {@link GoogleAuth.apiKey `apiKey`}. + * * @param apiKey The API key string * @param options An optional options object. * @returns A JWT loaded from the key */ - fromAPIKey(apiKey: string, options?: AuthClientOptions): JWT { - options = options || {}; - const client = new JWT(options); - client.fromAPIKey(apiKey); - return client; + fromAPIKey(apiKey: string, options: AuthClientOptions = {}): JWT { + return new JWT({...options, apiKey}); } /** @@ -996,19 +1004,26 @@ export class GoogleAuth { * provided configuration. If no options were passed, use Application * Default Credentials. */ - async getClient() { - 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); - } + async getClient(): Promise { + if (this.cachedCredential) { + return this.cachedCredential; + } else if (this.jsonContent) { + return this._cacheClientFromJSON(this.jsonContent, this.clientOptions); + } else if (this.keyFilename) { + const filePath = path.resolve(this.keyFilename); + const stream = fs.createReadStream(filePath); + return await this.fromStreamAsync(stream, this.clientOptions); + } else if (this.apiKey) { + const client = await this.fromAPIKey(this.apiKey, this.clientOptions); + client.scopes = this.scopes; + const {credential} = await this.#prepareAndCacheClient(client); + return credential; + } else { + const {credential} = await this.getApplicationDefaultAsync( + this.clientOptions + ); + return credential; } - return this.cachedCredential!; } /** diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 4c60c5f9..fb813a7f 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -528,8 +528,6 @@ export class OAuth2Client extends AuthClient { // TODO: refactor tests to make this private _clientSecret?: string; - apiKey?: string; - refreshHandler?: GetRefreshHandlerCallback; /** diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 3bb2ce95..afb56a3b 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -56,6 +56,7 @@ import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; import {AuthClient, DEFAULT_UNIVERSE} from '../src/auth/authclient'; import {ExternalAccountAuthorizedUserClient} from '../src/auth/externalAccountAuthorizedUserClient'; import {stringify} from 'querystring'; +import {GoogleAuthExceptionMessages} from '../src/auth/googleauth'; nock.disableNetConnect(); @@ -303,6 +304,30 @@ describe('googleauth', () => { assert.deepEqual(await auth.getRequestHeaders(''), customRequestHeaders); }); + it('should accept and use an `apiKey`', async () => { + const apiKey = 'myKey'; + const auth = new GoogleAuth({apiKey}); + const client = await auth.getClient(); + + assert.equal(client.apiKey, apiKey); + assert.deepEqual(await auth.getRequestHeaders(), { + 'X-Goog-Api-Key': apiKey, + }); + }); + + it('should not accept both an `apiKey` and `credentials`', async () => { + const apiKey = 'myKey'; + assert.throws( + () => + new GoogleAuth({ + credentials: {}, + // API key should supported via `clientOptions` + clientOptions: {apiKey}, + }), + new RangeError(GoogleAuthExceptionMessages.API_KEY_WITH_CREDENTIALS) + ); + }); + it('fromJSON should support the instantiated named export', () => { const result = auth.fromJSON(createJwtJSON()); assert(result); @@ -329,14 +354,6 @@ describe('googleauth', () => { assert.strictEqual(client.email, 'hello@youarecool.com'); }); - it('fromAPIKey should error given an invalid api key', () => { - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (auth as any).fromAPIKey(null); - }); - }); - it('should make a request with the api key', async () => { const scope = nock(BASE_URL) .post(ENDPOINT) @@ -1423,12 +1440,14 @@ describe('googleauth', () => { }); it('should pass options to the JWT constructor via constructor', async () => { + const apiKey = 'my-api-key'; const subject = 'science!'; const auth = new GoogleAuth({ keyFilename: './test/fixtures/private.json', - clientOptions: {subject}, + clientOptions: {apiKey, subject}, }); const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.apiKey, apiKey); assert.strictEqual(client.subject, subject); }); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index ebd42ef0..7c4d3446 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -74,6 +74,13 @@ describe('oauth2', () => { sandbox.restore(); }); + it('should accept and set an `apiKey`', () => { + const API_KEY = 'TEST_API_KEY'; + const client = new OAuth2Client({apiKey: API_KEY}); + + assert.equal(client.apiKey, API_KEY); + }); + it('should generate a valid consent page url', done => { const opts = { access_type: ACCESS_TYPE, From 243ce284bedd101a15a0e738a59a7db808c2ad3f Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 19 Aug 2024 14:45:12 -0700 Subject: [PATCH 548/662] feat: Group Concurrent `getClient` Requests (#1848) --- src/auth/googleauth.ts | 21 ++++++++++++++++++++- test/test.googleauth.ts | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 06ae466b..ae7a99a1 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -187,6 +187,10 @@ export class GoogleAuth { apiKey: string | null; cachedCredential: AnyAuthClient | T | null = null; + /** + * A pending {@link AuthClient}. Used for concurrent {@link GoogleAuth.getClient} calls. + */ + #pendingAuthClient: Promise | null = null; /** * Scopes populated by the client library by default. We differentiate between @@ -1007,7 +1011,22 @@ export class GoogleAuth { async getClient(): Promise { if (this.cachedCredential) { return this.cachedCredential; - } else if (this.jsonContent) { + } + + // Use an existing auth client request, or cache a new one + this.#pendingAuthClient = + this.#pendingAuthClient || this.#determineClient(); + + try { + return await this.#pendingAuthClient; + } finally { + // reset the pending auth client in case it is changed later + this.#pendingAuthClient = null; + } + } + + async #determineClient() { + if (this.jsonContent) { return this._cacheClientFromJSON(this.jsonContent, this.clientOptions); } else if (this.keyFilename) { const filePath = path.resolve(this.keyFilename); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index afb56a3b..495585df 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -2617,6 +2617,28 @@ describe('googleauth', () => { assert(actualClient instanceof ExternalAccountAuthorizedUserClient); }); + + it('should return the same instance for concurrent requests', async () => { + // Set up a mock to return path to a valid credentials file. + mockEnvVar( + 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/external-account-authorized-user-cred.json' + ); + const auth = new GoogleAuth(); + + let client: AuthClient | null = null; + const getClientCalls = await Promise.all([ + auth.getClient(), + auth.getClient(), + auth.getClient(), + ]); + + for (const resClient of getClientCalls) { + if (!client) client = resClient; + + assert(client === resClient); + } + }); }); describe('sign()', () => { From 5921cf91392795312dcf48923e80344812ea6494 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 19 Aug 2024 14:56:00 -0700 Subject: [PATCH 549/662] chore: sample doc cleanup (#1849) --- samples/authenticateAPIKey.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/samples/authenticateAPIKey.js b/samples/authenticateAPIKey.js index da9df2c1..026acab5 100644 --- a/samples/authenticateAPIKey.js +++ b/samples/authenticateAPIKey.js @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -/** - * Lists storage buckets by authenticating with ADC. - */ function main() { // [START apikeys_authenticate_api_key] From a56b198f23f869067ddadbfe936e64cc9e010d63 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:04:41 -0700 Subject: [PATCH 550/662] chore(main): release 9.14.0 (#1845) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Daniel Bankhead --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d19b896e..4bfc3a4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.14.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.13.0...v9.14.0) (2024-08-19) + + +### Features + +* Add `AnyAuthClient` type ([#1843](https://github.com/googleapis/google-auth-library-nodejs/issues/1843)) ([3ae120d](https://github.com/googleapis/google-auth-library-nodejs/commit/3ae120d0a45c95e36c59c9ac8286483938781f30)) +* Extend API Key Support ([#1835](https://github.com/googleapis/google-auth-library-nodejs/issues/1835)) ([5fc3bcc](https://github.com/googleapis/google-auth-library-nodejs/commit/5fc3bccacc74082f71983595dd7654b1b60be0f8)) +* Group Concurrent `getClient` Requests ([#1848](https://github.com/googleapis/google-auth-library-nodejs/issues/1848)) ([243ce28](https://github.com/googleapis/google-auth-library-nodejs/commit/243ce284bedd101a15a0e738a59a7db808c2ad3f)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v21 ([#1847](https://github.com/googleapis/google-auth-library-nodejs/issues/1847)) ([e9459f3](https://github.com/googleapis/google-auth-library-nodejs/commit/e9459f3d11418ce8afd4fe87cd92d4b2d06457ba)) + ## [9.13.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.12.0...v9.13.0) (2024-07-29) diff --git a/package.json b/package.json index c5124376..fe274c3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.13.0", + "version": "9.14.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index be6ae7a1..34da6b62 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^6.5.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^21.0.0", - "google-auth-library": "^9.13.0", + "google-auth-library": "^9.14.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From 85d9d6fe312f5ed68db22a28b84b6c8f257f9ec9 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 30 Aug 2024 14:39:09 -0700 Subject: [PATCH 551/662] perf(GoogleAuth): Improve Client Creation From Files/Streams Perf (#1856) --- src/auth/googleauth.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index ae7a99a1..3d08ec7f 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -787,15 +787,15 @@ export class GoogleAuth { 'Must pass in a stream containing the Google auth settings.' ); } - let s = ''; + const chunks: string[] = []; inputStream .setEncoding('utf8') .on('error', reject) - .on('data', chunk => (s += chunk)) + .on('data', chunk => chunks.push(chunk)) .on('end', () => { try { try { - const data = JSON.parse(s); + const data = JSON.parse(chunks.join('')); const r = this._cacheClientFromJSON(data, options); return resolve(r); } catch (err) { From 62b44e770f8a675b2d750883396ee31fda4b13ae Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:57:32 -0700 Subject: [PATCH 552/662] chore(main): release 9.14.1 (#1857) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bfc3a4a..b2848303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.14.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.14.0...v9.14.1) (2024-08-30) + + +### Performance Improvements + +* **GoogleAuth:** Improve Client Creation From Files/Streams Perf ([#1856](https://github.com/googleapis/google-auth-library-nodejs/issues/1856)) ([85d9d6f](https://github.com/googleapis/google-auth-library-nodejs/commit/85d9d6fe312f5ed68db22a28b84b6c8f257f9ec9)) + ## [9.14.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.13.0...v9.14.0) (2024-08-19) diff --git a/package.json b/package.json index fe274c3b..037d9fe6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.14.0", + "version": "9.14.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 34da6b62..aa0ece1a 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^6.5.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^21.0.0", - "google-auth-library": "^9.14.0", + "google-auth-library": "^9.14.1", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From 595979015b0a49b02a3e9dd607ff7f3ca4d5aeff Mon Sep 17 00:00:00 2001 From: aeitzman <12433791+aeitzman@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:26:48 -0700 Subject: [PATCH 553/662] docs: updates readme with working sample for AWS supplier. (#1854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: updates readme with working sample for AWS supplier. * Delete samples/authenticateWithCustomAwsSupplier.js * Changes in readme partial * Revert "docs: updates readme with working sample for AWS supplier." This reverts commit 80ad89a5f0087a97ab760617f84a4b16c0ed0cff. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 37 +++++++++++++++++++++++++++++++------ README.md | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 7b26d0d2..d39f3b1a 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -389,31 +389,56 @@ body: |- If you want to use AWS security credentials that cannot be retrieved using methods supported natively by this library, a custom AwsSecurityCredentialsSupplier implementation may be specified when creating an AWS client. The supplier must - return valid, unexpired AWS security credentials when called by the GCP credential. + return valid, unexpired AWS security credentials when called by the GCP credential. Currently, using ADC with your AWS + workloads is only supported with EC2. An example of a good use case for using a custom credential suppliers is when + your workloads are running in other AWS environments, such as ECS, EKS, Fargate, etc. Note that the client does not cache the returned AWS security credentials, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. ```ts + import { AwsClient, AwsSecurityCredentials, AwsSecurityCredentialsSupplier, ExternalAccountSupplierContext } from 'google-auth-library'; + import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; + import { Storage } from '@google-cloud/storage'; + class AwsSupplier implements AwsSecurityCredentialsSupplier { + private readonly region: string + + constructor(region: string) { + this.region = options.region; + } + async getAwsRegion(context: ExternalAccountSupplierContext): Promise { - // Return the current AWS region, i.e. 'us-east-2'. + // Return the AWS region i.e. "us-east-2". + return this.region } async getAwsSecurityCredentials( context: ExternalAccountSupplierContext ): Promise { - const audience = context.audience; - // Return valid AWS security credentials for the requested audience. + // Retrieve the AWS credentails. + const awsCredentialsProvider = fromNodeProviderChain(); + const awsCredentials = await awsCredentialsProvider(); + + // Parse the AWS credentials into a AWS security credentials instance and + // return them. + const awsSecurityCredentials = { + accessKeyId: awsCredentials.accessKeyId, + secretAccessKey: awsCredentials.secretAccessKey, + token: awsCredentials.sessionToken + } + return awsSecurityCredentials; } } const clientOptions = { audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type. - aws_security_credentials_supplier: new AwsSupplier() // Set the custom supplier. + aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier. } - const client = new AwsClient(clientOptions); + // Create a new Auth client and use it to create service client, i.e. storage. + const authClient = new AwsClient(clientOptions); + const storage = new Storage({ authClient }); ``` Where the [audience](https://cloud.google.com/iam/docs/best-practices-for-using-workload-identity-federation#provider-audience) is: `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` diff --git a/README.md b/README.md index e15d28fc..5d575201 100644 --- a/README.md +++ b/README.md @@ -433,31 +433,56 @@ Follow the detailed [instructions](https://cloud.google.com/iam/docs/access-reso If you want to use AWS security credentials that cannot be retrieved using methods supported natively by this library, a custom AwsSecurityCredentialsSupplier implementation may be specified when creating an AWS client. The supplier must -return valid, unexpired AWS security credentials when called by the GCP credential. +return valid, unexpired AWS security credentials when called by the GCP credential. Currently, using ADC with your AWS +workloads is only supported with EC2. An example of a good use case for using a custom credential suppliers is when +your workloads are running in other AWS environments, such as ECS, EKS, Fargate, etc. Note that the client does not cache the returned AWS security credentials, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. ```ts +import { AwsClient, AwsSecurityCredentials, AwsSecurityCredentialsSupplier, ExternalAccountSupplierContext } from 'google-auth-library'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; +import { Storage } from '@google-cloud/storage'; + class AwsSupplier implements AwsSecurityCredentialsSupplier { + private readonly region: string + + constructor(region: string) { + this.region = options.region; + } + async getAwsRegion(context: ExternalAccountSupplierContext): Promise { - // Return the current AWS region, i.e. 'us-east-2'. + // Return the AWS region i.e. "us-east-2". + return this.region } async getAwsSecurityCredentials( context: ExternalAccountSupplierContext ): Promise { - const audience = context.audience; - // Return valid AWS security credentials for the requested audience. + // Retrieve the AWS credentails. + const awsCredentialsProvider = fromNodeProviderChain(); + const awsCredentials = await awsCredentialsProvider(); + + // Parse the AWS credentials into a AWS security credentials instance and + // return them. + const awsSecurityCredentials = { + accessKeyId: awsCredentials.accessKeyId, + secretAccessKey: awsCredentials.secretAccessKey, + token: awsCredentials.sessionToken + } + return awsSecurityCredentials; } } const clientOptions = { audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type. - aws_security_credentials_supplier: new AwsSupplier() // Set the custom supplier. + aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier. } -const client = new AwsClient(clientOptions); +// Create a new Auth client and use it to create service client, i.e. storage. +const authClient = new AwsClient(clientOptions); +const storage = new Storage({ authClient }); ``` Where the [audience](https://cloud.google.com/iam/docs/best-practices-for-using-workload-identity-federation#provider-audience) is: `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` From 66f60bc0987622ab4d8c8fd12ec6bd84ad43672a Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 18:04:02 -0700 Subject: [PATCH 554/662] chore: update issue templates and codeowners (#1863) chore: update issue templates and codeowners Source-Link: https://github.com/googleapis/synthtool/commit/bf182cd41d9a7de56092cafcc7befe6b398332f6 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:a5af6af827a9fffba373151e1453b0498da288024cdd16477900dd42857a42e0 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 +- .github/CODEOWNERS | 7 +- .github/ISSUE_TEMPLATE/bug_report.yml | 99 +++++++++++++++++++ .../ISSUE_TEMPLATE/documentation_request.yml | 53 ++++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 53 ++++++++++ .github/ISSUE_TEMPLATE/processs_request.md | 5 + .github/ISSUE_TEMPLATE/questions.md | 8 ++ .github/auto-approve.yml | 4 +- .github/scripts/close-invalid-link.cjs | 53 ++++++++++ .github/scripts/close-unresponsive.cjs | 69 +++++++++++++ .github/scripts/remove-response-label.cjs | 33 +++++++ .github/workflows/issues-no-repro.yaml | 18 ++++ .github/workflows/response.yaml | 35 +++++++ README.md | 2 +- 14 files changed, 432 insertions(+), 11 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation_request.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/processs_request.md create mode 100644 .github/ISSUE_TEMPLATE/questions.md create mode 100644 .github/scripts/close-invalid-link.cjs create mode 100644 .github/scripts/close-unresponsive.cjs create mode 100644 .github/scripts/remove-response-label.cjs create mode 100644 .github/workflows/issues-no-repro.yaml create mode 100644 .github/workflows/response.yaml diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 9e90d54b..460f67f2 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:d920257482ca1cd72978f29f7d28765a9f8c758c21ee0708234db5cf4c5016c2 -# created: 2024-06-12T16:18:41.688792375Z + digest: sha256:a5af6af827a9fffba373151e1453b0498da288024cdd16477900dd42857a42e0 +# created: 2024-09-20T20:26:11.126243246Z diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 93cb5059..e777ff4c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,8 +5,5 @@ # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax -# The yoshi-nodejs team is the default owner for nodejs repositories. -* @googleapis/yoshi-nodejs @googleapis/nodejs-auth - -# The github automation team is the default owner for the auto-approve file. -.github/auto-approve.yml @googleapis/github-automation +# Unless specified, the jsteam is the default owner for nodejs repositories. +* @googleapis/nodejs-auth @googleapis/jsteam \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..bc451208 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,99 @@ +name: Bug Report +description: Create a report to help us improve +labels: + - bug +body: + - type: markdown + attributes: + value: > + **PLEASE READ**: If you have a support contract with Google, please + create an issue in the [support + console](https://cloud.google.com/support/) instead of filing on GitHub. + This will ensure a timely response. Otherwise, please make sure to + follow the steps below. + - type: checkboxes + attributes: + label: Please make sure you have searched for information in the following + guides. + options: + - label: "Search the issues already opened: + https://github.com/GoogleCloudPlatform/google-cloud-node/issues" + required: true + - label: "Search StackOverflow: + http://stackoverflow.com/questions/tagged/google-cloud-platform+nod\ + e.js" + required: true + - label: "Check our Troubleshooting guide: + https://googlecloudplatform.github.io/google-cloud-node/#/docs/guid\ + es/troubleshooting" + required: true + - label: "Check our FAQ: + https://googlecloudplatform.github.io/google-cloud-node/#/docs/guid\ + es/faq" + required: true + - label: "Check our libraries HOW-TO: + https://github.com/googleapis/gax-nodejs/blob/main/client-libraries\ + .md" + required: true + - label: "Check out our authentication guide: + https://github.com/googleapis/google-auth-library-nodejs" + required: true + - label: "Check out handwritten samples for many of our APIs: + https://github.com/GoogleCloudPlatform/nodejs-docs-samples" + required: true + - type: textarea + attributes: + label: > + A screenshot that you have tested with "Try this API". + description: > + As our client libraries are mostly autogenerated, we kindly request + that you test whether your issue is with the client library, or with the + API itself. To do so, please search for your API + here: https://developers.google.com/apis-explorer and attempt to + reproduce the issue in the given method. Please include a screenshot of + the response in "Try this API". This response should NOT match the current + behavior you are experiencing. If the behavior is the same, it means + that you are likely experiencing a bug with the API itself. In that + case, please submit an issue to the API team, either by submitting an + issue in its issue tracker https://cloud.google.com/support/docs/issue-trackers, or by + submitting an issue in its linked tracker in the .repo-metadata.json + file + validations: + required: true + - type: input + attributes: + label: > + Link to the code that reproduces this issue. A link to a **public** Github Repository or gist with a minimal + reproduction. + description: > + **Skipping this or providing an invalid link will result in the issue being closed** + validations: + required: true + - type: textarea + attributes: + label: > + A step-by-step description of how to reproduce the issue, based on + the linked reproduction. + description: > + Screenshots can be provided in the issue body below. + placeholder: | + 1. Start the application in development (next dev) + 2. Click X + 3. Y will happen + validations: + required: true + - type: textarea + attributes: + label: A clear and concise description of what the bug is, and what you + expected to happen. + placeholder: Following the steps from the previous section, I expected A to + happen, but I observed B instead + validations: + required: true + + - type: textarea + attributes: + label: A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + placeholder: 'Documentation here(link) states that B should happen instead of A' + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/documentation_request.yml b/.github/ISSUE_TEMPLATE/documentation_request.yml new file mode 100644 index 00000000..2e571c2e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_request.yml @@ -0,0 +1,53 @@ +name: Documentation Requests +description: Requests for more information +body: + - type: markdown + attributes: + value: > + Please use this issue type to log documentation requests against the library itself. + These requests should involve documentation on Github (`.md` files), and should relate to the library + itself. If you have questions or documentation requests for an API, please + reach out to the API tracker itself. + + Please submit an issue to the API team, either by submitting an + issue in its issue tracker https://cloud.google.com/support/docs/issue-trackers), or by + submitting an issue in its linked tracker in the .repo-metadata.json + file in the API under packages/* ([example]()). + You can also submit a request to documentation on cloud.google.com itself with the "Send Feedback" + on the bottom of the page. + + + Please note that documentation requests and questions for specific APIs + will be closed. + - type: checkboxes + attributes: + label: Please make sure you have searched for information in the following + guides. + options: + - label: "Search the issues already opened: + https://github.com/GoogleCloudPlatform/google-cloud-node/issues" + required: true + - label: "Check our Troubleshooting guide: + https://googlecloudplatform.github.io/google-cloud-node/#/docs/guid\ + es/troubleshooting" + required: true + - label: "Check our FAQ: + https://googlecloudplatform.github.io/google-cloud-node/#/docs/guid\ + es/faq" + required: true + - label: "Check our libraries HOW-TO: + https://github.com/googleapis/gax-nodejs/blob/main/client-libraries\ + .md" + required: true + - label: "Check out our authentication guide: + https://github.com/googleapis/google-auth-library-nodejs" + required: true + - label: "Check out handwritten samples for many of our APIs: + https://github.com/GoogleCloudPlatform/nodejs-docs-samples" + required: true + - type: textarea + attributes: + label: > + Documentation Request + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..765444dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,53 @@ +name: Feature Request +description: Suggest an idea for this library +labels: + - feature request +body: + - type: markdown + attributes: + value: > + **PLEASE READ**: If you have a support contract with Google, please + create an issue in the [support + console](https://cloud.google.com/support/) instead of filing on GitHub. + This will ensure a timely response. Otherwise, please make sure to + follow the steps below. + - type: textarea + attributes: + label: > + A screenshot that you have tested with "Try this API". + description: > + As our client libraries are mostly autogenerated, we kindly request + that you test whether your feature request is with the client library, or with the + API itself. To do so, please search for your API + here: https://developers.google.com/apis-explorer and attempt to + reproduce the issue in the given method. Please include a screenshot of + the response in "Try this API". This response should NOT match the current + behavior you are experiencing. If the behavior is the same, it means + that you are likely requesting a feature for the API itself. In that + case, please submit an issue to the API team, either by submitting an + issue in its issue tracker https://cloud.google.com/support/docs/issue-trackers, or by + submitting an issue in its linked tracker in the .repo-metadata.json + file in the API under packages/* ([example]()) + + Example of library specific issues would be: retry strategies, authentication questions, or issues with typings. + Examples of API issues would include: expanding method parameter types, adding functionality to an API. + validations: + required: true + - type: textarea + attributes: + label: > + What would you like to see in the library? + description: > + Screenshots can be provided in the issue body below. + placeholder: | + 1. Set up authentication like so + 2. Run the program like so + 3. X would be nice to happen + + - type: textarea + attributes: + label: Describe alternatives you've considered + + - type: textarea + attributes: + label: Additional context/notes \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/processs_request.md b/.github/ISSUE_TEMPLATE/processs_request.md new file mode 100644 index 00000000..9f88fc1f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/processs_request.md @@ -0,0 +1,5 @@ +--- +name: Process Request +about: Submit a process request to the library. Process requests are any requests related to library infrastructure, including CI/CD, publishing, releasing, etc. This issue template should primarily used by internal members. + +--- \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/questions.md b/.github/ISSUE_TEMPLATE/questions.md new file mode 100644 index 00000000..62c1dd1b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/questions.md @@ -0,0 +1,8 @@ +--- +name: Question +about: If you have a question, please use Discussions + +--- + +If you have a general question that goes beyond the library itself, we encourage you to use [Discussions](https://github.com//discussions) +to engage with fellow community members! diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml index ec51b072..7cba0af6 100644 --- a/.github/auto-approve.yml +++ b/.github/auto-approve.yml @@ -1,4 +1,2 @@ processes: - - "NodeDependency" - - "OwlBotTemplateChangesNode" - - "OwlBotPRsNode" \ No newline at end of file + - "NodeDependency" \ No newline at end of file diff --git a/.github/scripts/close-invalid-link.cjs b/.github/scripts/close-invalid-link.cjs new file mode 100644 index 00000000..ba7d5137 --- /dev/null +++ b/.github/scripts/close-invalid-link.cjs @@ -0,0 +1,53 @@ +// Copyright 2024 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. + +async function closeIssue(github, owner, repo, number) { + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: number, + body: 'Issue was opened with an invalid reproduction link. Please make sure the repository is a valid, publicly-accessible github repository, and make sure the url is complete (example: https://github.com/googleapis/google-cloud-node)' + }); + await github.rest.issues.update({ + owner: owner, + repo: repo, + issue_number: number, + state: 'closed' + }); +} +module.exports = async ({github, context}) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + const number = context.issue.number; + + const issue = await github.rest.issues.get({ + owner: owner, + repo: repo, + issue_number: number, + }); + + const isBugTemplate = issue.data.body.includes('Link to the code that reproduces this issue'); + + if (isBugTemplate) { + try { + const link = issue.data.body.split('\n')[18].match(/(https?:\/\/g?i?s?t?\.?github.com\/.*)/); + const isValidLink = (await fetch(link)).ok; + if (!isValidLink) { + await closeIssue(github, owner, repo, number); + } + } catch (err) { + await closeIssue(github, owner, repo, number); + } + } +}; diff --git a/.github/scripts/close-unresponsive.cjs b/.github/scripts/close-unresponsive.cjs new file mode 100644 index 00000000..142dc126 --- /dev/null +++ b/.github/scripts/close-unresponsive.cjs @@ -0,0 +1,69 @@ +// Copyright 2024 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. + +function labeledEvent(data) { + return data.event === 'labeled' && data.label.name === 'needs more info'; + } + + const numberOfDaysLimit = 15; + const close_message = `This has been closed since a request for information has \ + not been answered for ${numberOfDaysLimit} days. It can be reopened when the \ + requested information is provided.`; + + module.exports = async ({github, context}) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + + const issues = await github.rest.issues.listForRepo({ + owner: owner, + repo: repo, + labels: 'needs more info', + }); + const numbers = issues.data.map((e) => e.number); + + for (const number of numbers) { + const events = await github.paginate( + github.rest.issues.listEventsForTimeline, + { + owner: owner, + repo: repo, + issue_number: number, + }, + (response) => response.data.filter(labeledEvent) + ); + + const latest_response_label = events[events.length - 1]; + + const created_at = new Date(latest_response_label.created_at); + const now = new Date(); + const diff = now - created_at; + const diffDays = diff / (1000 * 60 * 60 * 24); + + if (diffDays > numberOfDaysLimit) { + await github.rest.issues.update({ + owner: owner, + repo: repo, + issue_number: number, + state: 'closed', + }); + + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: number, + body: close_message, + }); + } + } + }; diff --git a/.github/scripts/remove-response-label.cjs b/.github/scripts/remove-response-label.cjs new file mode 100644 index 00000000..887cf349 --- /dev/null +++ b/.github/scripts/remove-response-label.cjs @@ -0,0 +1,33 @@ +// Copyright 2024 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. + +module.exports = async ({ github, context }) => { + const commenter = context.actor; + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const author = issue.data.user.login; + const labels = issue.data.labels.map((e) => e.name); + + if (author === commenter && labels.includes('needs more info')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'needs more info', + }); + } + }; diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml new file mode 100644 index 00000000..442a46bc --- /dev/null +++ b/.github/workflows/issues-no-repro.yaml @@ -0,0 +1,18 @@ +name: invalid_link +on: + issues: + types: [opened, reopened] + +jobs: + close: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/github-script@v7 + with: + script: | + const script = require('./.github/scripts/close-invalid-link.cjs') + await script({github, context}) diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml new file mode 100644 index 00000000..6ed37326 --- /dev/null +++ b/.github/workflows/response.yaml @@ -0,0 +1,35 @@ +name: no_response +on: + schedule: + - cron: '30 1 * * *' # Run every day at 01:30 + workflow_dispatch: + issue_comment: + +jobs: + close: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/github-script@v7 + with: + script: | + const script = require('./.github/scripts/close-unresponsive.cjs') + await script({github, context}) + + remove_label: + if: github.event_name == 'issue_comment' + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/github-script@v7 + with: + script: | + const script = require('./.github/scripts/remove-response-label.cjs') + await script({github, context}) diff --git a/README.md b/README.md index 5d575201..7873cb27 100644 --- a/README.md +++ b/README.md @@ -1464,4 +1464,4 @@ See [LICENSE](https://github.com/googleapis/google-auth-library-nodejs/blob/main [projects]: https://console.cloud.google.com/project [billing]: https://support.google.com/cloud/answer/6293499#enable-billing -[auth]: https://cloud.google.com/docs/authentication/getting-started +[auth]: https://cloud.google.com/docs/authentication/external/set-up-adc-local From 8adb44c738b88bfc44e57b0694c3815d138a40e5 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 7 Oct 2024 12:07:39 -0700 Subject: [PATCH 555/662] fix: Disable Universe Domain Check (#1878) * fix: Disable Universe Domain Check * chore: compodoc nonsense --- package.json | 1 + src/auth/googleauth.ts | 6 ------ test/test.googleauth.ts | 12 +----------- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 037d9fe6..45ded0e6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", + "pdfmake": "0.2.12", "puppeteer": "^21.0.0", "sinon": "^18.0.0", "ts-loader": "^8.0.0", diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 3d08ec7f..b211d7c9 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -457,12 +457,6 @@ export class GoogleAuth { // Determine if we're running on GCE. if (await this._checkIsGCE()) { - // set universe domain for Compute client - if (!originalOrCamelOptions(options).get('universe_domain')) { - options.universeDomain = - await this.getUniverseDomainFromMetadataServer(); - } - (options as ComputeOptions).scopes = this.getAnyScopes(); return await this.#prepareAndCacheClient(new Compute(options)); } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 495585df..09853af4 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1149,9 +1149,8 @@ describe('googleauth', () => { it('getCredentials should get metadata from the server when running on GCE', async () => { const clientEmail = 'test-creds@test-creds.iam.gserviceaccount.com'; - const universeDomain = 'my-amazing-universe.com'; const scopes = [ - nockIsGCE({universeDomain}), + nockIsGCE(), createGetProjectIdNock(), nock(host).get(svcAccountPath).reply(200, clientEmail, HEADERS), ]; @@ -1160,7 +1159,6 @@ describe('googleauth', () => { const body = await auth.getCredentials(); assert.ok(body); assert.strictEqual(body.client_email, clientEmail); - assert.strictEqual(body.universe_domain, universeDomain); assert.strictEqual(body.private_key, undefined); scopes.forEach(s => s.done()); }); @@ -1644,14 +1642,6 @@ describe('googleauth', () => { assert.notEqual(universe_domain, DEFAULT_UNIVERSE); assert.equal(await auth.getUniverseDomain(), universe_domain); }); - - it('should use the metadata service if on GCP', async () => { - const universeDomain = 'my.universe.com'; - const scope = nockIsGCE({universeDomain}); - - assert.equal(await auth.getUniverseDomain(), universeDomain); - await scope.done(); - }); }); function mockApplicationDefaultCredentials(path: string) { From 8336aa2ef58f3fad0adbc882ae9a38d11f8512a6 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 10 Oct 2024 00:31:13 +0200 Subject: [PATCH 556/662] chore(deps): update actions/checkout digest to eef6144 (#1884) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f761a6ee..7a90c6cf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [14, 16, 18, 20] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: actions/setup-node@v4 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: actions/setup-node@v4 with: node-version: 14 From 2453a46db7709892ee6e1c385e746bc1f8f50d98 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:56:06 -0700 Subject: [PATCH 557/662] chore(main): release 9.14.2 (#1883) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2848303..ba31befa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.14.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.14.1...v9.14.2) (2024-10-09) + + +### Bug Fixes + +* Disable Universe Domain Check ([#1878](https://github.com/googleapis/google-auth-library-nodejs/issues/1878)) ([8adb44c](https://github.com/googleapis/google-auth-library-nodejs/commit/8adb44c738b88bfc44e57b0694c3815d138a40e5)) + ## [9.14.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.14.0...v9.14.1) (2024-08-30) diff --git a/package.json b/package.json index 45ded0e6..3478fa56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.14.1", + "version": "9.14.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index aa0ece1a..3950c444 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^6.5.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^21.0.0", - "google-auth-library": "^9.14.1", + "google-auth-library": "^9.14.2", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From a65d8a11450fdc0f69ea228def462e5a77beecd5 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:08:06 -0400 Subject: [PATCH 558/662] chore: update links in github issue templates and switch to using compodoc (#1876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update links in github issue templates * chore: update links in github issue templates Source-Link: https://github.com/googleapis/synthtool/commit/38fa49fb668c2beb27f598ad3dda2aa46b8a10ed Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:609822e3c09b7a1bd90b99655904609f162cc15acb4704f1edf778284c36f429 * Delete .github/ISSUE_TEMPLATE/bug_report.md * Delete .github/ISSUE_TEMPLATE/documentation_request.yml * Delete .github/ISSUE_TEMPLATE/feature_request.md * Delete .github/ISSUE_TEMPLATE/question.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Update package.json * Update .jsdoc.js * Update owlbot.py --------- Co-authored-by: Owl Bot Co-authored-by: Daniel Bankhead Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 +-- .github/ISSUE_TEMPLATE/bug_report.md | 38 ---------------------- .github/ISSUE_TEMPLATE/bug_report.yml | 12 +++---- .github/ISSUE_TEMPLATE/feature_request.md | 18 ---------- .github/ISSUE_TEMPLATE/processs_request.md | 5 ++- .github/ISSUE_TEMPLATE/question.md | 12 ------- .github/scripts/close-invalid-link.cjs | 5 ++- .jsdoc.js | 2 +- owlbot.py | 1 + package.json | 6 ++-- 10 files changed, 20 insertions(+), 83 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 460f67f2..24943e11 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:a5af6af827a9fffba373151e1453b0498da288024cdd16477900dd42857a42e0 -# created: 2024-09-20T20:26:11.126243246Z + digest: sha256:609822e3c09b7a1bd90b99655904609f162cc15acb4704f1edf778284c36f429 +# created: 2024-10-01T19:34:30.797530443Z diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 85050fe9..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -labels: 'type: bug, priority: p2' ---- - -Thanks for stopping by to let us know something could be better! - -**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. - -1) Is this a client library issue or a product issue? -This is the client library for . We will only be able to assist with issues that pertain to the behaviors of this library. If the issue you're experiencing is due to the behavior of the product itself, please visit the [ Support page]() to reach the most relevant engineers. - -2) Did someone already solve this? - - Search the issues already opened: https://github.com/googleapis/google-auth-library-nodejs/issues - - Search the issues on our "catch-all" repository: https://github.com/googleapis/google-cloud-node - - Search or ask on StackOverflow (engineers monitor these tags): http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js - -3) Do you have a support contract? -Please create an issue in the [support console](https://cloud.google.com/support/) to ensure a timely response. - -If the support paths suggested above still do not result in a resolution, please provide the following details. - -#### Environment details - - - OS: - - Node.js version: - - npm version: - - `google-auth-library` version: - -#### Steps to reproduce - - 1. ? - 2. ? - -Making sure to follow these steps will guarantee the quickest resolution possible. - -Thanks! diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bc451208..24b5a410 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -24,12 +24,12 @@ body: e.js" required: true - label: "Check our Troubleshooting guide: - https://googlecloudplatform.github.io/google-cloud-node/#/docs/guid\ - es/troubleshooting" + https://github.com/googleapis/google-cloud-node/blob/main/docs/trou\ + bleshooting.md" required: true - label: "Check our FAQ: - https://googlecloudplatform.github.io/google-cloud-node/#/docs/guid\ - es/faq" + https://github.com/googleapis/google-cloud-node/blob/main/docs/faq.\ + md" required: true - label: "Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries\ @@ -55,9 +55,9 @@ body: behavior you are experiencing. If the behavior is the same, it means that you are likely experiencing a bug with the API itself. In that case, please submit an issue to the API team, either by submitting an - issue in its issue tracker https://cloud.google.com/support/docs/issue-trackers, or by + issue in its issue tracker (https://cloud.google.com/support/docs/issue-trackers), or by submitting an issue in its linked tracker in the .repo-metadata.json - file + file validations: required: true - type: input diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index b0327dfa..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this library -labels: 'type: feature request, priority: p3' ---- - -Thanks for stopping by to let us know something could be better! - -**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. - - **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - **Describe the solution you'd like** -A clear and concise description of what you want to happen. - **Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - **Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/processs_request.md b/.github/ISSUE_TEMPLATE/processs_request.md index 9f88fc1f..45682e8f 100644 --- a/.github/ISSUE_TEMPLATE/processs_request.md +++ b/.github/ISSUE_TEMPLATE/processs_request.md @@ -1,5 +1,4 @@ --- name: Process Request -about: Submit a process request to the library. Process requests are any requests related to library infrastructure, including CI/CD, publishing, releasing, etc. This issue template should primarily used by internal members. - ---- \ No newline at end of file +about: Submit a process request to the library. Process requests are any requests related to library infrastructure, for example CI/CD, publishing, releasing, broken links. +--- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 97323113..00000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: Question -about: Ask a question -labels: 'type: question, priority: p3' ---- - -Thanks for stopping by to ask us a question! Please make sure to include: -- What you're trying to do -- What code you've already tried -- Any error messages you're getting - -**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. diff --git a/.github/scripts/close-invalid-link.cjs b/.github/scripts/close-invalid-link.cjs index ba7d5137..d7a3688e 100644 --- a/.github/scripts/close-invalid-link.cjs +++ b/.github/scripts/close-invalid-link.cjs @@ -40,9 +40,12 @@ module.exports = async ({github, context}) => { const isBugTemplate = issue.data.body.includes('Link to the code that reproduces this issue'); if (isBugTemplate) { + console.log(`Issue ${number} is a bug template`) try { - const link = issue.data.body.split('\n')[18].match(/(https?:\/\/g?i?s?t?\.?github.com\/.*)/); + const link = issue.data.body.split('\n')[18].match(/(https?:\/\/(gist\.)?github.com\/.*)/)[0]; + console.log(`Issue ${number} contains this link: ${link}`) const isValidLink = (await fetch(link)).ok; + console.log(`Issue ${number} has a ${isValidLink ? 'valid' : 'invalid'} link`) if (!isValidLink) { await closeIssue(github, owner, repo, number); } diff --git a/.jsdoc.js b/.jsdoc.js index 5596afee..a521abe2 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -31,7 +31,7 @@ module.exports = { source: { excludePattern: '(^|\\/|\\\\)[._]', include: [ - 'src', + 'build/src', ], includePattern: '\\.js$' }, diff --git a/owlbot.py b/owlbot.py index 2ff4b9eb..d41bc272 100644 --- a/owlbot.py +++ b/owlbot.py @@ -22,5 +22,6 @@ node.owlbot_main( templates_excludes=[ ".github/workflows/ci.yaml", + ".github/ISSUE_TEMPLATE/bug_report.yml" ], ) diff --git a/package.json b/package.json index 3478fa56..fae900e5 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "jws": "^4.0.0" }, "devDependencies": { - "@compodoc/compodoc": "1.1.23", "@types/base64-js": "^1.2.5", "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", @@ -42,6 +41,9 @@ "execa": "^5.0.0", "gts": "^5.0.0", "is-docker": "^2.0.0", + "jsdoc": "^4.0.0", + "jsdoc-fresh": "^3.0.0", + "jsdoc-region-tag": "^3.0.0", "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^2.0.0", @@ -76,7 +78,7 @@ "compile": "tsc -p .", "fix": "gts fix", "pretest": "npm run compile -- --sourceMap", - "docs": "compodoc src/", + "docs": "jsdoc -c .jsdoc.json", "samples-setup": "cd samples/ && npm link ../ && npm run setup && cd ../", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "system-test": "mocha build/system-test --timeout 60000", From 902bf8b7faf8f7a0735011c252907282f550cd14 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 1 Nov 2024 13:15:24 -0700 Subject: [PATCH 559/662] feat: Impersonated Universe Domain Support (#1875) * feat: Impersonated w/ Universe Support * docs: jsdoc/tsdoc fix * feat: `useEmailAzp` * chore: compodoc nonsense * chore: for compodoc nonsense * chore: typo * refactor: Explicit Universe Domains should throw for `Impersonated` * feat: Support `external_account` in `fromImpersonatedJSON` * feat: Improve `Impersonated` Support --- src/auth/authclient.ts | 3 ++ src/auth/googleauth.ts | 25 ++++++----- src/auth/impersonated.ts | 23 +++++++++- src/auth/refreshclient.ts | 11 +++++ test/test.googleauth.ts | 88 ++++++++++++++++++++++++++++++++++++++- test/test.impersonated.ts | 72 ++++++++++++++++++++++++++++++++ 6 files changed, 206 insertions(+), 16 deletions(-) diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 2f2b384c..b9de5c95 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -55,6 +55,9 @@ interface AuthJSONOptions { /** * The default service domain for a given Cloud universe. + * + * @example + * 'googleapis.com' */ universe_domain: string; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index b211d7c9..90ecf49b 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -290,7 +290,7 @@ export class GoogleAuth { } } - /* + /** * A private method for finding and caching a projectId. * * Supports environments in order of precedence: @@ -632,9 +632,7 @@ export class GoogleAuth { ); } - // Create source client for impersonation - const sourceClient = new UserRefreshClient(); - sourceClient.fromJSON(json.source_credentials); + const sourceClient = this.fromJSON(json.source_credentials); if (json.service_account_impersonation_url?.length > 256) { /** @@ -646,10 +644,11 @@ export class GoogleAuth { ); } - // Extreact service account from service_account_impersonation_url - const targetPrincipal = /(?[^/]+):generateAccessToken$/.exec( - json.service_account_impersonation_url - )?.groups?.target; + // Extract service account from service_account_impersonation_url + const targetPrincipal = + /(?[^/]+):(generateAccessToken|generateIdToken)$/.exec( + json.service_account_impersonation_url + )?.groups?.target; if (!targetPrincipal) { throw new RangeError( @@ -659,18 +658,18 @@ export class GoogleAuth { const targetScopes = this.getAnyScopes() ?? []; - const client = new Impersonated({ + return new Impersonated({ ...json, - delegates: json.delegates ?? [], - sourceClient: sourceClient, - targetPrincipal: targetPrincipal, + sourceClient, + targetPrincipal, targetScopes: Array.isArray(targetScopes) ? targetScopes : [targetScopes], }); - return client; } /** * Create a credentials instance using the given input options. + * This client is not cached. + * * @param json The input object. * @param options The JWT or UserRefresh options for the client * @returns JWT or UserRefresh Client with data diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index 2b4a2555..0dc0e7ec 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -23,6 +23,7 @@ import {AuthClient} from './authclient'; import {IdTokenProvider} from './idtokenclient'; import {GaxiosError} from 'gaxios'; import {SignBlobResponse} from './googleauth'; +import {originalOrCamelOptions} from '../util'; export interface ImpersonatedOptions extends OAuth2ClientOptions { /** @@ -124,7 +125,22 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { this.delegates = options.delegates ?? []; this.targetScopes = options.targetScopes ?? []; this.lifetime = options.lifetime ?? 3600; - this.endpoint = options.endpoint ?? 'https://iamcredentials.googleapis.com'; + + const usingExplicitUniverseDomain = + !!originalOrCamelOptions(options).get('universe_domain'); + + if (!usingExplicitUniverseDomain) { + // override the default universe with the source's universe + this.universeDomain = this.sourceClient.universeDomain; + } else if (this.sourceClient.universeDomain !== this.universeDomain) { + // non-default universe and is not matching the source - this could be a credential leak + throw new RangeError( + `Universe domain ${this.sourceClient.universeDomain} in source credentials does not match ${this.universeDomain} universe domain set for impersonated credentials.` + ); + } + + this.endpoint = + options.endpoint ?? `https://iamcredentials.${this.universeDomain}`; } /** @@ -132,7 +148,8 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { * * {@link https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob Reference Documentation} * @param blobToSign String to sign. - * @return denoting the keyyID and signedBlob in base64 string + * + * @returns A {@link SignBlobResponse} denoting the keyID and signedBlob in base64 string */ async sign(blobToSign: string): Promise { await this.sourceClient.getAccessToken(); @@ -224,7 +241,9 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { delegates: this.delegates, audience: targetAudience, includeEmail: options?.includeEmail ?? true, + useEmailAzp: options?.includeEmail ?? true, }; + const res = await this.sourceClient.request({ ...Impersonated.RETRY_CONFIG, url: u, diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index eca95d1b..93c07d49 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -183,4 +183,15 @@ export class UserRefreshClient extends OAuth2Client { }); }); } + + /** + * Create a UserRefreshClient credentials instance using the given input + * options. + * @param json The input object. + */ + static fromJSON(json: JWTInput): UserRefreshClient { + const client = new UserRefreshClient(); + client.fromJSON(json); + return client; + } } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 09853af4..ebea50c0 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -41,6 +41,7 @@ import { ExternalAccountClientOptions, RefreshOptions, Impersonated, + IdentityPoolClient, } from '../src'; import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; @@ -52,11 +53,16 @@ import { mockStsTokenExchange, saEmail, } from './externalclienthelper'; -import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; +import { + BaseExternalAccountClient, + EXTERNAL_ACCOUNT_TYPE, +} from '../src/auth/baseexternalclient'; import {AuthClient, DEFAULT_UNIVERSE} from '../src/auth/authclient'; import {ExternalAccountAuthorizedUserClient} from '../src/auth/externalAccountAuthorizedUserClient'; import {stringify} from 'querystring'; import {GoogleAuthExceptionMessages} from '../src/auth/googleauth'; +import {IMPERSONATED_ACCOUNT_TYPE} from '../src/auth/impersonated'; +import {USER_REFRESH_ACCOUNT_TYPE} from '../src/auth/refreshclient'; nock.disableNetConnect(); @@ -1656,6 +1662,86 @@ describe('googleauth', () => { .reply(200, {}); } describe('for impersonated types', () => { + describe('source clients', () => { + it('should support a variety of source clients', async () => { + const serviceAccountImpersonationURLBase = + 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@test-project.iam.gserviceaccount.com:generateToken'; + const samples: { + creds: { + type: typeof IMPERSONATED_ACCOUNT_TYPE; + service_account_impersonation_url: string; + source_credentials: {}; + }; + expectedSource: typeof AuthClient; + }[] = [ + // USER_TO_SERVICE_ACCOUNT_JSON + { + creds: { + type: IMPERSONATED_ACCOUNT_TYPE, + service_account_impersonation_url: new URL( + './test@test-project.iam.gserviceaccount.com:generateAccessToken', + serviceAccountImpersonationURLBase + ).toString(), + source_credentials: { + client_id: 'client', + client_secret: 'secret', + refresh_token: 'refreshToken', + type: USER_REFRESH_ACCOUNT_TYPE, + }, + }, + expectedSource: UserRefreshClient, + }, + // SERVICE_ACCOUNT_TO_SERVICE_ACCOUNT_JSON + { + creds: { + type: IMPERSONATED_ACCOUNT_TYPE, + service_account_impersonation_url: new URL( + './test@test-project.iam.gserviceaccount.com:generateIdToken', + serviceAccountImpersonationURLBase + ).toString(), + source_credentials: { + type: 'service_account', + client_email: 'google@auth.library', + private_key: privateKey, + }, + }, + expectedSource: JWT, + }, + // EXTERNAL_ACCOUNT_TO_SERVICE_ACCOUNT_JSON + { + creds: { + type: IMPERSONATED_ACCOUNT_TYPE, + service_account_impersonation_url: new URL( + './test@test-project.iam.gserviceaccount.com:generateIdToken', + serviceAccountImpersonationURLBase + ).toString(), + source_credentials: { + type: EXTERNAL_ACCOUNT_TYPE, + audience: 'audience', + subject_token_type: 'access_token', + token_url: 'https://sts.googleapis.com/v1/token', + credential_source: {url: 'https://example.com/token'}, + }, + }, + expectedSource: IdentityPoolClient, + }, + ]; + + const auth = new GoogleAuth(); + for (const {creds, expectedSource} of samples) { + const client = auth.fromJSON(creds); + + assert(client instanceof Impersonated); + + // This is a private prop - we will refactor/remove in the future + assert( + (client as unknown as {sourceClient: {}}).sourceClient instanceof + expectedSource + ); + } + }); + }); + describe('for impersonated credentials signing', () => { const now = new Date().getTime(); const saSuccessResponse = { diff --git a/test/test.impersonated.ts b/test/test.impersonated.ts index d8bc2b06..12ca0e65 100644 --- a/test/test.impersonated.ts +++ b/test/test.impersonated.ts @@ -97,6 +97,76 @@ describe('impersonated', () => { scopes.forEach(s => s.done()); }); + it('should inherit a `universeDomain` from the source client', async () => { + const universeDomain = 'my.universe.com'; + + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + const scopes = [ + nock(url).get('/').reply(200), + createGTokenMock({ + access_token: 'abc123', + }), + nock(`https://iamcredentials.${universeDomain}`) + .post( + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', + (body: ImpersonatedCredentialRequest) => { + assert.strictEqual(body.lifetime, '30s'); + assert.deepStrictEqual(body.delegates, []); + assert.deepStrictEqual(body.scope, [ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + return true; + } + ) + .reply(200, { + accessToken: 'universe-token', + expireTime: tomorrow.toISOString(), + }), + ]; + + const sourceClient = createSampleJWTClient(); + + // Use a simple API key for this test. No need to get too fancy. + sourceClient.apiKey = 'ABC'; + delete sourceClient.subject; + + sourceClient.universeDomain = universeDomain; + + const impersonated = new Impersonated({ + sourceClient, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + + await impersonated.request({url}); + assert.strictEqual(impersonated.credentials.access_token, 'universe-token'); + + scopes.forEach(s => s.done()); + }); + + it("should throw if an explicit `universeDomain` does not equal the source's `universeDomain`", async () => { + const universeDomain = 'my.universe.com'; + const otherUniverseDomain = 'not-my.universe.com'; + + const sourceClient = createSampleJWTClient(); + sourceClient.universeDomain = otherUniverseDomain; + + assert.throws(() => { + new Impersonated({ + sourceClient, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + lifetime: 30, + delegates: [], + targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], + universeDomain, + }); + }, /does not match/); + }); + it('should not request impersonated credentials on second request', async () => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); @@ -383,10 +453,12 @@ describe('impersonated', () => { delegates: string[]; audience: string; includeEmail: boolean; + useEmailAzp: true; }) => { assert.strictEqual(body.audience, expectedAudience); assert.strictEqual(body.includeEmail, expectedIncludeEmail); assert.deepStrictEqual(body.delegates, expectedDeligates); + assert.strictEqual(body.useEmailAzp, true); return true; } ) From 08978822e1b7b5961f0e355df51d738e012be392 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 08:38:07 -0800 Subject: [PATCH 560/662] chore(main): release 9.15.0 (#1891) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba31befa..a6ccfd34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.15.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.14.2...v9.15.0) (2024-11-01) + + +### Features + +* Impersonated Universe Domain Support ([#1875](https://github.com/googleapis/google-auth-library-nodejs/issues/1875)) ([902bf8b](https://github.com/googleapis/google-auth-library-nodejs/commit/902bf8b7faf8f7a0735011c252907282f550cd14)) + ## [9.14.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.14.1...v9.14.2) (2024-10-09) diff --git a/package.json b/package.json index fae900e5..2bc32c04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.14.2", + "version": "9.15.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 3950c444..d246f52b 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^6.5.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^21.0.0", - "google-auth-library": "^9.14.2", + "google-auth-library": "^9.15.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From f52a190f955a1525850f1430d0d33f8d230ade67 Mon Sep 17 00:00:00 2001 From: VarunChillara <19792111+varun27896@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:55:50 -0600 Subject: [PATCH 561/662] docs(readme): add example for service_account_impersonation_url in clientOptions (#1902) * docs(readme): add service_account_impersonation_url example for AwsSupplier The service_account_impersonation_url must be passed in the clientOptions when using a custom AwsSupplier to avoid permissions errors. Updated the README to include this information. * docs(config): add service_account_impersonation_url example to .partials.yml Updated the .partials.yml file to include the service_account_impersonation_url configuration example, ensuring consistency with the README file. --- .readme-partials.yaml | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index d39f3b1a..151dd1dd 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -434,6 +434,7 @@ body: |- audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type. aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier. + service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken', // Set the service account impersonation url. } // Create a new Auth client and use it to create service client, i.e. storage. diff --git a/README.md b/README.md index 7873cb27..1da188e4 100644 --- a/README.md +++ b/README.md @@ -478,6 +478,7 @@ const clientOptions = { audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type. aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier. + service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken', // Set the service account impersonation url. } // Create a new Auth client and use it to create service client, i.e. storage. From 970b135824a016a068aa6741d26db6f3cdce3284 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:07:20 -0800 Subject: [PATCH 562/662] docs: sample fix (#1907) --- samples/authenticateAPIKey.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/authenticateAPIKey.js b/samples/authenticateAPIKey.js index 026acab5..b42ba031 100644 --- a/samples/authenticateAPIKey.js +++ b/samples/authenticateAPIKey.js @@ -28,8 +28,8 @@ function main() { const language = new LanguageServiceClient({apiKey}); // Alternatively: - // const auth = new GoogleAuth({apiKey}); // const {GoogleAuth} = require('google-auth-library'); + // const auth = new GoogleAuth({apiKey}); // const language = new LanguageServiceClient({auth}); const text = 'Hello, world!'; From 12f2c87266de0a3ccd33e6b4993cab3537f9a242 Mon Sep 17 00:00:00 2001 From: Ryunosuke Hayashi Date: Fri, 10 Jan 2025 10:08:05 +0900 Subject: [PATCH 563/662] fix: resolve typo in document (#1901) --- .readme-partials.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 151dd1dd..6a8bb79f 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -997,7 +997,7 @@ body: |- } const clientOptions = { - audience: '//iam.googleapis.com/locations/global/workforcePools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. + audience: '//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', // Set the subject token type. subject_token_supplier: new CustomSupplier() // Set the custom supplier. } @@ -1005,11 +1005,11 @@ body: |- const client = new CustomSupplier(clientOptions); ``` - Where the audience is: `//iam.googleapis.com/locations/global/workforcePools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` + Where the audience is: `//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID` Where the following variables need to be substituted: - * `WORKFORCE_POOL_ID`: The worforce pool ID. + * `$WORKFORCE_POOL_ID`: The worforce pool ID. * `$PROVIDER_ID`: The provider ID. and the workforce pool user project is the project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). From f08cd65f1c0598241e4d58e9cef2a203dbda69be Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:39:47 -0800 Subject: [PATCH 564/662] test: Improve Kitchen Test Logging (#1914) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: Improve Kitchen Test Logging - Adds missing STDERR logging for improved debugging, which was missing for a while - Removes `execa`, as it is unneeded and outdated * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: add 'error' event handle * test: documentation --------- Co-authored-by: Owl Bot --- README.md | 6 +++--- package.json | 1 - system-test/test.kitchen.ts | 31 ++++++++++++++++++++++++++----- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1da188e4..81252a1b 100644 --- a/README.md +++ b/README.md @@ -1041,7 +1041,7 @@ class CustomSupplier implements SubjectTokenSupplier { } const clientOptions = { - audience: '//iam.googleapis.com/locations/global/workforcePools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. + audience: '//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', // Set the subject token type. subject_token_supplier: new CustomSupplier() // Set the custom supplier. } @@ -1049,11 +1049,11 @@ const clientOptions = { const client = new CustomSupplier(clientOptions); ``` -Where the audience is: `//iam.googleapis.com/locations/global/workforcePools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID` +Where the audience is: `//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID` Where the following variables need to be substituted: -* `WORKFORCE_POOL_ID`: The worforce pool ID. +* `$WORKFORCE_POOL_ID`: The worforce pool ID. * `$PROVIDER_ID`: The provider ID. and the workforce pool user project is the project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project). diff --git a/package.json b/package.json index 2bc32c04..c61ef089 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "chai": "^4.2.0", "cheerio": "1.0.0-rc.12", "codecov": "^3.0.2", - "execa": "^5.0.0", "gts": "^5.0.0", "is-docker": "^2.0.0", "jsdoc": "^4.0.0", diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index e818ef26..1d10eed9 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -14,13 +14,13 @@ import * as assert from 'assert'; import {describe, it, afterEach} from 'mocha'; -import * as execa from 'execa'; import * as fs from 'fs'; import * as mv from 'mv'; import {ncp} from 'ncp'; import * as os from 'os'; import * as path from 'path'; import {promisify} from 'util'; +import {spawn} from 'child_process'; const mvp = promisify(mv) as {} as (...args: string[]) => Promise; const ncpp = promisify(ncp); @@ -29,18 +29,39 @@ const keep = !!process.env.GALN_KEEP_TEMPDIRS; const pkg = require('../../package.json'); let stagingDir: string; + +/** + * Spawns and runs a command asynchronously. + * + * @param params params to pass to {@link spawn} + */ +async function run(...params: Parameters) { + const command = spawn(...params); + + await new Promise((resolve, reject) => { + // Unlike `exec`/`execFile`, this keeps the order of STDOUT/STDERR in case they were interweaved; + // making it easier to debug and follow along. + command.stdout?.on('data', console.log); + command.stderr?.on('data', console.error); + command.on('close', (code, signal) => { + return code === 0 ? resolve() : reject({code, signal}); + }); + command.on('error', reject); + }); +} + async function packAndInstall() { stagingDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'google-auth-library-nodejs-pack-') ); - await execa('npm', ['pack'], {stdio: 'inherit'}); + await run('npm', ['pack'], {}); const tarball = `${pkg.name}-${pkg.version}.tgz`; // stagingPath can be on another filesystem so fs.rename() will fail // with EXDEV, hence we use `mv` module here. await mvp(tarball, `${stagingDir}/google-auth-library.tgz`); await ncpp('system-test/fixtures/kitchen', `${stagingDir}/`); - await execa('npm', ['install'], {cwd: `${stagingDir}/`, stdio: 'inherit'}); + await run('npm', ['install'], {cwd: `${stagingDir}/`}); } describe('pack and install', () => { @@ -61,10 +82,10 @@ describe('pack and install', () => { this.timeout(40000); await packAndInstall(); // we expect npm install is executed in the before hook - await execa('npx', ['webpack'], {cwd: `${stagingDir}/`, stdio: 'inherit'}); + await run('npx', ['webpack'], {cwd: `${stagingDir}/`}); const bundle = path.join(stagingDir, 'dist', 'bundle.min.js'); // ensure it is a non-empty bundle - assert(fs.statSync(bundle).size); + assert(fs.statSync(bundle).size, 'Size should not be empty'); }); /** From 178b4812c69062dcd06dedd38bbcec6b55bcef10 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:42:18 -0800 Subject: [PATCH 565/662] docs: Provide Guidance for Untrusted JSON creds (#1909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Provide Guidance for Untrusted JSON creds * chore: artifact * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: Update .readme-partials.yaml * chore: Update googleauth.ts * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 2 ++ README.md | 2 ++ src/auth/googleauth.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 6a8bb79f..27077dc5 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -308,6 +308,8 @@ body: |- main().catch(console.error); ``` + **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). + #### Using a Proxy You can set the `HTTPS_PROXY` or `https_proxy` environment variables to proxy HTTPS requests. When `HTTPS_PROXY` or `https_proxy` are set, they will be used to proxy SSL requests that do not have an explicit proxy configuration option present. diff --git a/README.md b/README.md index 81252a1b..4e9255ef 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,8 @@ async function main() { main().catch(console.error); ``` +**Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). + #### Using a Proxy You can set the `HTTPS_PROXY` or `https_proxy` environment variables to proxy HTTPS requests. When `HTTPS_PROXY` or `https_proxy` are set, they will be used to proxy SSL requests that do not have an explicit proxy configuration option present. diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 90ecf49b..dc8786a5 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -670,6 +670,8 @@ export class GoogleAuth { * Create a credentials instance using the given input options. * This client is not cached. * + * **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to {@link https://cloud.google.com/docs/authentication/external/externally-sourced-credentials Validate credential configurations from external sources}. + * * @param json The input object. * @param options The JWT or UserRefresh options for the client * @returns JWT or UserRefresh Client with data From 33211bc88c65514abbd6b14e3398963491ca594c Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Thu, 23 Jan 2025 10:58:47 -0800 Subject: [PATCH 566/662] chore: improve system-test log format (#1921) --- system-test/test.kitchen.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 1d10eed9..4b8ae875 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -38,11 +38,25 @@ let stagingDir: string; async function run(...params: Parameters) { const command = spawn(...params); + function stdout(str: string) { + const prefix = '\n>>> STDOUT: '; + console.log(prefix + str.replace(/\n/g, prefix)); + } + + function stderr(str: string) { + const prefix = '\n>>> STDERR: '; + console.error(prefix + str.replace(/\n/g, prefix)); + } + await new Promise((resolve, reject) => { // Unlike `exec`/`execFile`, this keeps the order of STDOUT/STDERR in case they were interweaved; // making it easier to debug and follow along. - command.stdout?.on('data', console.log); - command.stderr?.on('data', console.error); + command.stdout?.setEncoding('utf8'); + command.stderr?.setEncoding('utf8'); + + command.stdout?.on('data', stdout); + command.stderr?.on('data', stderr); + command.on('close', (code, signal) => { return code === 0 ? resolve() : reject({code, signal}); }); From 75adc4e8831e0cbf32590445d9345ed334e6af16 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:57:37 -0800 Subject: [PATCH 567/662] chore: pin `engine.io` to prevent `cookie` breaking change (#1922) cookie v1 doesn't support Node 18> --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c61ef089..691ba6ac 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "chai": "^4.2.0", "cheerio": "1.0.0-rc.12", "codecov": "^3.0.2", + "engine.io": "6.6.2", "gts": "^5.0.0", "is-docker": "^2.0.0", "jsdoc": "^4.0.0", From c59f8b47d5b177d65244e95be1a516dc70991cdb Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:55:20 -0800 Subject: [PATCH 568/662] docs: JSON credential best practices (#1923) --- src/auth/googleauth.ts | 4 ++++ src/auth/jwtclient.ts | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index dc8786a5..7889e916 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -108,6 +108,10 @@ export interface GoogleAuthOptions { * Object containing client_email and private_key properties, or the * external account client options. * Cannot be used with {@link GoogleAuthOptions.apiKey `apiKey`}. + * + * @remarks + * + * **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to {@link https://cloud.google.com/docs/authentication/external/externally-sourced-credentials Validate credential configurations from external sources}. */ credentials?: JWTInput | ExternalAccountClientOptions; diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index a2d753d2..76fe00f7 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -303,6 +303,10 @@ export class JWT extends OAuth2Client implements IdTokenProvider { /** * Create a JWT credentials instance using the given input options. * @param json The input object. + * + * @remarks + * + * **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to {@link https://cloud.google.com/docs/authentication/external/externally-sourced-credentials Validate credential configurations from external sources}. */ fromJSON(json: JWTInput): void { if (!json) { @@ -333,6 +337,10 @@ export class JWT extends OAuth2Client implements IdTokenProvider { * Create a JWT credentials instance using the given input stream. * @param inputStream The input stream. * @param callback Optional callback. + * + * @remarks + * + * **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to {@link https://cloud.google.com/docs/authentication/external/externally-sourced-credentials Validate credential configurations from external sources}. */ fromStream(inputStream: stream.Readable): Promise; fromStream( From e86f8620c8e2d0114b8e83489b98bafd3bebacb3 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Thu, 23 Jan 2025 23:30:03 -0800 Subject: [PATCH 569/662] test: Increase timeout for `webpack` (#1924) --- system-test/test.kitchen.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 4b8ae875..da566d1d 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -30,6 +30,11 @@ const pkg = require('../../package.json'); let stagingDir: string; +/** + * 2 minutes + */ +const BUILD_TEST_TIMEOUT_MS = 2 * 60_000; + /** * Spawns and runs a command asynchronously. * @@ -87,13 +92,15 @@ describe('pack and install', () => { // npm, once in a blue moon, fails during pack process. If this happens, // we should be safe to retry. this.retries(3); - this.timeout(40000); + this.timeout(BUILD_TEST_TIMEOUT_MS); + await packAndInstall(); }); it('should be able to webpack the library', async function () { this.retries(3); - this.timeout(40000); + this.timeout(BUILD_TEST_TIMEOUT_MS); + await packAndInstall(); // we expect npm install is executed in the before hook await run('npx', ['webpack'], {cwd: `${stagingDir}/`}); From 69be99032c05997adab5be81bf0f17ae2fe25676 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 08:21:28 -0800 Subject: [PATCH 570/662] chore(main): release 9.15.1 (#1908) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ccfd34..dc942a7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [9.15.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.15.0...v9.15.1) (2025-01-24) + + +### Bug Fixes + +* Resolve typo in document ([#1901](https://github.com/googleapis/google-auth-library-nodejs/issues/1901)) ([12f2c87](https://github.com/googleapis/google-auth-library-nodejs/commit/12f2c87266de0a3ccd33e6b4993cab3537f9a242)) + ## [9.15.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.14.2...v9.15.0) (2024-11-01) diff --git a/package.json b/package.json index 691ba6ac..a7e3231e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.15.0", + "version": "9.15.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index d246f52b..87dde05f 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^6.5.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^21.0.0", - "google-auth-library": "^9.15.0", + "google-auth-library": "^9.15.1", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From b0c3a43124860530a567a3529f8ac41b6c7d20c5 Mon Sep 17 00:00:00 2001 From: HubertJan <70658889+HubertJan@users.noreply.github.com> Date: Sat, 1 Feb 2025 04:42:07 +0100 Subject: [PATCH 571/662] refactor!: Move Base AuthClient Types to authclient.ts (#1774) * refactor!: Move Base AuthClient Types to authclient.ts * style: Fix lints * chore: merge conflict --------- Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> Co-authored-by: Daniel Bankhead --- src/auth/authclient.ts | 10 +++++++++- src/auth/awsclient.ts | 1 + src/auth/awsrequestsigner.ts | 2 +- src/auth/baseexternalclient.ts | 8 ++++++-- .../defaultawssecuritycredentialssupplier.ts | 2 +- src/auth/downscopedclient.ts | 8 ++++++-- src/auth/externalAccountAuthorizedUserClient.ts | 3 +-- src/auth/googleauth.ts | 9 +++++++-- src/auth/idtokenclient.ts | 2 +- src/auth/jwtaccess.ts | 2 +- src/auth/oauth2client.ts | 16 ++++++---------- src/auth/passthrough.ts | 3 +-- src/auth/stscredentials.ts | 2 +- test/externalclienthelper.ts | 2 +- test/test.downscopedclient.ts | 2 +- test/test.oauth2common.ts | 2 +- 16 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index b9de5c95..1eddeb06 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -17,7 +17,6 @@ import {Gaxios, GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; import {DefaultTransporter, Transporter} from '../transporters'; import {Credentials} from './credentials'; -import {GetAccessTokenResponse, Headers} from './oauth2client'; import {OriginalAndCamel, originalOrCamelOptions} from '../util'; /** @@ -307,3 +306,12 @@ export abstract class AuthClient }; } } + +export interface Headers { + [index: string]: string; +} + +export interface GetAccessTokenResponse { + token?: string | null; + res?: GaxiosResponse | null; +} diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index d1c3155f..c3fadf68 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -18,6 +18,7 @@ import { BaseExternalAccountClientOptions, ExternalAccountSupplierContext, } from './baseexternalclient'; + import {AuthClientOptions} from './authclient'; import {DefaultAwsSecurityCredentialsSupplier} from './defaultawssecuritycredentialssupplier'; import {originalOrCamelOptions, SnakeToCamelObject} from '../util'; diff --git a/src/auth/awsrequestsigner.ts b/src/auth/awsrequestsigner.ts index 4bb5a151..cb0126c3 100644 --- a/src/auth/awsrequestsigner.ts +++ b/src/auth/awsrequestsigner.ts @@ -14,7 +14,7 @@ import {GaxiosOptions} from 'gaxios'; -import {Headers} from './oauth2client'; +import {Headers} from './authclient'; import {Crypto, createCrypto, fromArrayBufferToHex} from '../crypto/crypto'; type HttpMethod = diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 26436979..8df6e2b5 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -22,9 +22,13 @@ import { import * as stream from 'stream'; import {Credentials} from './credentials'; -import {AuthClient, AuthClientOptions} from './authclient'; +import { + AuthClient, + AuthClientOptions, + GetAccessTokenResponse, + Headers, +} from './authclient'; import {BodyResponseCallback, Transporter} from '../transporters'; -import {GetAccessTokenResponse, Headers} from './oauth2client'; import * as sts from './stscredentials'; import {ClientAuthentication} from './oauth2common'; import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; diff --git a/src/auth/defaultawssecuritycredentialssupplier.ts b/src/auth/defaultawssecuritycredentialssupplier.ts index 12d48d3f..2c06c134 100644 --- a/src/auth/defaultawssecuritycredentialssupplier.ts +++ b/src/auth/defaultawssecuritycredentialssupplier.ts @@ -17,7 +17,7 @@ import {Gaxios, GaxiosOptions} from 'gaxios'; import {Transporter} from '../transporters'; import {AwsSecurityCredentialsSupplier} from './awsclient'; import {AwsSecurityCredentials} from './awsrequestsigner'; -import {Headers} from './oauth2client'; +import {Headers} from './authclient'; /** * Interface defining the AWS security-credentials endpoint response. diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 3005e7cc..d4338f0b 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -22,9 +22,13 @@ import * as stream from 'stream'; import {BodyResponseCallback} from '../transporters'; import {Credentials} from './credentials'; -import {AuthClient, AuthClientOptions} from './authclient'; +import { + AuthClient, + AuthClientOptions, + GetAccessTokenResponse, + Headers, +} from './authclient'; -import {GetAccessTokenResponse, Headers} from './oauth2client'; import * as sts from './stscredentials'; /** diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index 24a480c0..29812c4b 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {AuthClient, AuthClientOptions} from './authclient'; -import {Headers} from './oauth2client'; +import {AuthClient, AuthClientOptions, Headers} from './authclient'; import { ClientAuthentication, getErrorFromOAuthErrorResponse, diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 7889e916..7036ba97 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -28,7 +28,7 @@ import {CredentialBody, ImpersonatedJWTInput, JWTInput} from './credentials'; import {IdTokenClient} from './idtokenclient'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; -import {Headers, OAuth2ClientOptions} from './oauth2client'; +import {OAuth2ClientOptions} from './oauth2client'; import { UserRefreshClient, UserRefreshClientOptions, @@ -47,7 +47,12 @@ import { EXTERNAL_ACCOUNT_TYPE, BaseExternalAccountClient, } from './baseexternalclient'; -import {AuthClient, AuthClientOptions, DEFAULT_UNIVERSE} from './authclient'; +import { + AuthClient, + AuthClientOptions, + DEFAULT_UNIVERSE, + Headers, +} from './authclient'; import { EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, ExternalAccountAuthorizedUserClient, diff --git a/src/auth/idtokenclient.ts b/src/auth/idtokenclient.ts index 5bd62b52..24d1f859 100644 --- a/src/auth/idtokenclient.ts +++ b/src/auth/idtokenclient.ts @@ -13,8 +13,8 @@ // limitations under the License. import {Credentials} from './credentials'; +import {Headers} from './authclient'; import { - Headers, OAuth2Client, OAuth2ClientOptions, RequestMetadataResponse, diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index b7c61147..0cabbcbb 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -16,7 +16,7 @@ import * as jws from 'jws'; import * as stream from 'stream'; import {JWTInput} from './credentials'; -import {Headers} from './oauth2client'; +import {Headers} from './authclient'; import {LRUCache} from '../util'; const DEFAULT_HEADER: jws.Header = { diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index fb813a7f..e9af50c2 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -25,7 +25,12 @@ import * as formatEcdsa from 'ecdsa-sig-formatter'; import {createCrypto, JwkCertificate, hasBrowserCrypto} from '../crypto/crypto'; import {BodyResponseCallback} from '../transporters'; -import {AuthClient, AuthClientOptions} from './authclient'; +import { + AuthClient, + AuthClientOptions, + GetAccessTokenResponse, + Headers, +} from './authclient'; import {CredentialRequest, Credentials} from './credentials'; import {LoginTicket, TokenPayload} from './loginticket'; /** @@ -54,10 +59,6 @@ export interface PublicKeys { [index: string]: string; } -export interface Headers { - [index: string]: string; -} - export enum CodeChallengeMethod { Plain = 'plain', S256 = 'S256', @@ -359,11 +360,6 @@ export interface GetAccessTokenCallback { ): void; } -export interface GetAccessTokenResponse { - token?: string | null; - res?: GaxiosResponse | null; -} - export interface RefreshAccessTokenCallback { ( err: GaxiosError | null, diff --git a/src/auth/passthrough.ts b/src/auth/passthrough.ts index bde50cba..6d6b7c17 100644 --- a/src/auth/passthrough.ts +++ b/src/auth/passthrough.ts @@ -13,8 +13,7 @@ // limitations under the License. import {GaxiosOptions} from 'gaxios'; -import {AuthClient} from './authclient'; -import {GetAccessTokenResponse, Headers} from './oauth2client'; +import {AuthClient, GetAccessTokenResponse, Headers} from './authclient'; /** * An AuthClient without any Authentication information. Useful for: diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 291da246..f5a596ac 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -16,7 +16,7 @@ import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; import * as querystring from 'querystring'; import {DefaultTransporter, Transporter} from '../transporters'; -import {Headers} from './oauth2client'; +import {Headers} from './authclient'; import { ClientAuthentication, OAuthClientAuthHandler, diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 9cc62adf..4e13bafa 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import * as nock from 'nock'; import * as qs from 'querystring'; -import {GetAccessTokenResponse} from '../src/auth/oauth2client'; +import {GetAccessTokenResponse} from '../src/auth/authclient'; import {OAuthErrorResponse} from '../src/auth/oauth2common'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; import { diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index 067155b3..f717cc2b 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -31,7 +31,7 @@ import { OAuthErrorResponse, getErrorFromOAuthErrorResponse, } from '../src/auth/oauth2common'; -import {GetAccessTokenResponse, Headers} from '../src/auth/oauth2client'; +import {GetAccessTokenResponse, Headers} from '../src/auth/authclient'; nock.disableNetConnect(); diff --git a/test/test.oauth2common.ts b/test/test.oauth2common.ts index 2afd159a..c0700190 100644 --- a/test/test.oauth2common.ts +++ b/test/test.oauth2common.ts @@ -17,7 +17,7 @@ import {describe, it} from 'mocha'; import * as assert from 'assert'; import * as querystring from 'querystring'; -import {Headers} from '../src/auth/oauth2client'; +import {Headers} from '../src/auth/authclient'; import { ClientAuthentication, OAuthClientAuthHandler, From 659496bb6d50f21e333c034f4f58cac7fa1404b0 Mon Sep 17 00:00:00 2001 From: Aldrin <53973174+Dhoni77@users.noreply.github.com> Date: Sat, 1 Feb 2025 09:24:19 +0530 Subject: [PATCH 572/662] refactor: Remove Transporter and DefaultTransporter from GoogleAuth class (#1688) Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- src/auth/googleauth.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 7036ba97..e70e186e 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -21,8 +21,6 @@ import * as path from 'path'; import * as stream from 'stream'; import {Crypto, createCrypto} from '../crypto/crypto'; -import {DefaultTransporter, Transporter} from '../transporters'; - import {Compute, ComputeOptions} from './computeclient'; import {CredentialBody, ImpersonatedJWTInput, JWTInput} from './credentials'; import {IdTokenClient} from './idtokenclient'; @@ -171,8 +169,6 @@ export const GoogleAuthExceptionMessages = { } as const; export class GoogleAuth { - transporter?: Transporter; - /** * Caches a value indicating whether the auth layer is running on Google * Compute Engine. @@ -210,11 +206,6 @@ export class GoogleAuth { private scopes?: string | string[]; private clientOptions: AuthClientOptions = {}; - /** - * Export DefaultTransporter as a static property of the class. - */ - static DefaultTransporter = DefaultTransporter; - /** * Configuration is resolved in the following order of precedence: * - {@link GoogleAuthOptions.credentials `credentials`} From 4f1dc0476ccbfba26043aa2dab6673bc03a0787d Mon Sep 17 00:00:00 2001 From: Aldrin <53973174+Dhoni77@users.noreply.github.com> Date: Sat, 1 Feb 2025 10:53:20 +0530 Subject: [PATCH 573/662] refactor!: remove DEFAULT_UNIVERSE from BaseExternalClient (#1690) * chore: remove DEFAULT_UNIVERSE from BaseExternalClient * chore: update unit tests * chore: fix formatting --------- Co-authored-by: Daniel Bankhead --- src/auth/baseexternalclient.ts | 5 ----- test/test.baseexternalclient.ts | 3 +-- test/test.externalaccountauthorizeduserclient.ts | 6 ++---- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 8df6e2b5..6f480979 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -73,11 +73,6 @@ const DEFAULT_TOKEN_URL = 'https://sts.{universeDomain}/v1/token'; // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); -/** - * For backwards compatibility. - */ -export {DEFAULT_UNIVERSE} from './authclient'; - /** * Shared options used to build {@link ExternalAccountClient} and * {@link ExternalAccountAuthorizedUserClient}. diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 71c4ec7b..c1af2b9b 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -23,7 +23,6 @@ import { EXPIRATION_TIME_OFFSET, BaseExternalAccountClient, BaseExternalAccountClientOptions, - DEFAULT_UNIVERSE, } from '../src/auth/baseexternalclient'; import { OAuthErrorResponse, @@ -40,7 +39,7 @@ import { mockStsTokenExchange, getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; -import {AuthClientOptions} from '../src/auth/authclient'; +import {AuthClientOptions, DEFAULT_UNIVERSE} from '../src/auth/authclient'; nock.disableNetConnect(); diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 7b74ebc6..81ec5fd2 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -23,15 +23,13 @@ import { ExternalAccountAuthorizedUserClient, ExternalAccountAuthorizedUserClientOptions, } from '../src/auth/externalAccountAuthorizedUserClient'; -import { - DEFAULT_UNIVERSE, - EXPIRATION_TIME_OFFSET, -} from '../src/auth/baseexternalclient'; +import {EXPIRATION_TIME_OFFSET} from '../src/auth/baseexternalclient'; import {GaxiosError, GaxiosResponse} from 'gaxios'; import { getErrorFromOAuthErrorResponse, OAuthErrorResponse, } from '../src/auth/oauth2common'; +import {DEFAULT_UNIVERSE} from '../src/auth/authclient'; nock.disableNetConnect(); From c10c3f2be5a6d0b80b52b0b914bffe5dfe9da763 Mon Sep 17 00:00:00 2001 From: piaxc Date: Fri, 31 Jan 2025 23:04:35 -0800 Subject: [PATCH 574/662] docs: Update README to Point to Newer Instructions (#1861) * Update README.md Remove mention of service account key download and point to correct place for ADC setup. * chore: use readme-partials.yaml --------- Co-authored-by: Daniel Bankhead Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- .readme-partials.yaml | 14 +++----------- README.md | 15 +++------------ 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 27077dc5..3590f619 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -13,20 +13,12 @@ body: |- - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials - This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. - They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. + This library provides an implementation of [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials) for Node.js. ADC provides a simple way to get credentials for use in calling Google APIs. How you [set up ADC](https://cloud.google.com/docs/authentication/provide-credentials-adc) depends on the environment where your code is running. - Application Default Credentials also support workload identity federation to access Google Cloud resources from non-Google Cloud platforms including Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). Workload identity federation is recommended for non-Google Cloud environments as it avoids the need to download, manage and store service account private keys locally, see: [Workload Identity Federation](#workload-identity-federation). + ADC is best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. - #### Download your Service Account Credentials JSON file - - To use Application Default Credentials, You first need to download a set of JSON credentials for your project. Go to **APIs & Auth** > **Credentials** in the [Google Developers Console](https://console.cloud.google.com/) and select **Service account** from the **Add credentials** dropdown. - - > This file is your *only copy* of these credentials. It should never be - > committed with your source code, and should be stored securely. - - Once downloaded, store the path to this file in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. + Application Default Credentials also supports Workload Identity Federation to access Google Cloud resources from non-Google Cloud platforms including Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). Workload Identity Federation is recommended for non-Google Cloud environments as it avoids the need to download, manage and store service account private keys locally, see: [Workload Identity Federation](#workload-identity-federation). #### Enable the API you want to use diff --git a/README.md b/README.md index 4e9255ef..441c2748 100644 --- a/README.md +++ b/README.md @@ -57,20 +57,11 @@ This library provides a variety of ways to authenticate to your Google services. - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials -This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs. +This library provides an implementation of [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials) for Node.js. ADC provides a simple way to get credentials for use in calling Google APIs. How you [set up ADC](https://cloud.google.com/docs/authentication/provide-credentials-adc) depends on the environment where your code is running. -They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. +ADC is best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. -Application Default Credentials also support workload identity federation to access Google Cloud resources from non-Google Cloud platforms including Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). Workload identity federation is recommended for non-Google Cloud environments as it avoids the need to download, manage and store service account private keys locally, see: [Workload Identity Federation](#workload-identity-federation). - -#### Download your Service Account Credentials JSON file - -To use Application Default Credentials, You first need to download a set of JSON credentials for your project. Go to **APIs & Auth** > **Credentials** in the [Google Developers Console](https://console.cloud.google.com/) and select **Service account** from the **Add credentials** dropdown. - -> This file is your *only copy* of these credentials. It should never be -> committed with your source code, and should be stored securely. - -Once downloaded, store the path to this file in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. +Application Default Credentials also supports Workload Identity Federation to access Google Cloud resources from non-Google Cloud platforms including Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC). Workload Identity Federation is recommended for non-Google Cloud environments as it avoids the need to download, manage and store service account private keys locally, see: [Workload Identity Federation](#workload-identity-federation). #### Enable the API you want to use From 2ef88c37e8a0293dd16e719eaaffcfd58e50553b Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:13:43 -0800 Subject: [PATCH 575/662] chore: fix `npm` for Node v18 samples tests (#1897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: fix `npm` for Node v18 samples tests chore: fix `npm` for samples tests Source-Link: https://github.com/googleapis/synthtool/commit/4d752428d93b18b69c28acdbd9aa821a517db73a Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:0d39e59663287ae929c1d4ccf8ebf7cef9946826c9b86eda7e85d8d752dbb584 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .github/release-trigger.yml | 1 + .kokoro/common.cfg | 2 +- .kokoro/continuous/node18/common.cfg | 24 ++++++++++++++++++++++ .kokoro/continuous/node18/lint.cfg | 4 ++++ .kokoro/continuous/node18/samples-test.cfg | 12 +++++++++++ .kokoro/continuous/node18/system-test.cfg | 12 +++++++++++ .kokoro/continuous/node18/test.cfg | 0 .kokoro/presubmit/node18/common.cfg | 24 ++++++++++++++++++++++ .kokoro/presubmit/node18/samples-test.cfg | 12 +++++++++++ .kokoro/presubmit/node18/system-test.cfg | 12 +++++++++++ .kokoro/presubmit/node18/test.cfg | 0 .kokoro/release/docs-devsite.cfg | 2 +- .kokoro/release/docs.cfg | 2 +- .kokoro/release/docs.sh | 2 +- .kokoro/release/publish.cfg | 2 +- .kokoro/samples-test.sh | 6 ++++-- .kokoro/system-test.sh | 2 +- .kokoro/test.bat | 2 +- .kokoro/test.sh | 2 +- .kokoro/trampoline_v2.sh | 2 +- README.md | 1 + 22 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 .kokoro/continuous/node18/common.cfg create mode 100644 .kokoro/continuous/node18/lint.cfg create mode 100644 .kokoro/continuous/node18/samples-test.cfg create mode 100644 .kokoro/continuous/node18/system-test.cfg create mode 100644 .kokoro/continuous/node18/test.cfg create mode 100644 .kokoro/presubmit/node18/common.cfg create mode 100644 .kokoro/presubmit/node18/samples-test.cfg create mode 100644 .kokoro/presubmit/node18/system-test.cfg create mode 100644 .kokoro/presubmit/node18/test.cfg diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 24943e11..39a62ca6 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:609822e3c09b7a1bd90b99655904609f162cc15acb4704f1edf778284c36f429 -# created: 2024-10-01T19:34:30.797530443Z + digest: sha256:0d39e59663287ae929c1d4ccf8ebf7cef9946826c9b86eda7e85d8d752dbb584 +# created: 2024-11-21T22:39:44.342569463Z diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml index d4ca9418..f0c6a773 100644 --- a/.github/release-trigger.yml +++ b/.github/release-trigger.yml @@ -1 +1,2 @@ enabled: true +multiScmName: google-auth-library-nodejs \ No newline at end of file diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index eb5e5c10..16c6b60e 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -16,7 +16,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/continuous/node18/common.cfg b/.kokoro/continuous/node18/common.cfg new file mode 100644 index 00000000..16c6b60e --- /dev/null +++ b/.kokoro/continuous/node18/common.cfg @@ -0,0 +1,24 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" +} +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/test.sh" +} diff --git a/.kokoro/continuous/node18/lint.cfg b/.kokoro/continuous/node18/lint.cfg new file mode 100644 index 00000000..49ffcd82 --- /dev/null +++ b/.kokoro/continuous/node18/lint.cfg @@ -0,0 +1,4 @@ +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/lint.sh" +} diff --git a/.kokoro/continuous/node18/samples-test.cfg b/.kokoro/continuous/node18/samples-test.cfg new file mode 100644 index 00000000..9571f5db --- /dev/null +++ b/.kokoro/continuous/node18/samples-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/samples-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/continuous/node18/system-test.cfg b/.kokoro/continuous/node18/system-test.cfg new file mode 100644 index 00000000..83d64098 --- /dev/null +++ b/.kokoro/continuous/node18/system-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/system-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/continuous/node18/test.cfg b/.kokoro/continuous/node18/test.cfg new file mode 100644 index 00000000..e69de29b diff --git a/.kokoro/presubmit/node18/common.cfg b/.kokoro/presubmit/node18/common.cfg new file mode 100644 index 00000000..16c6b60e --- /dev/null +++ b/.kokoro/presubmit/node18/common.cfg @@ -0,0 +1,24 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" +} +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/test.sh" +} diff --git a/.kokoro/presubmit/node18/samples-test.cfg b/.kokoro/presubmit/node18/samples-test.cfg new file mode 100644 index 00000000..9571f5db --- /dev/null +++ b/.kokoro/presubmit/node18/samples-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/samples-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node18/system-test.cfg b/.kokoro/presubmit/node18/system-test.cfg new file mode 100644 index 00000000..83d64098 --- /dev/null +++ b/.kokoro/presubmit/node18/system-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/google-auth-library-nodejs/.kokoro/system-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node18/test.cfg b/.kokoro/presubmit/node18/test.cfg new file mode 100644 index 00000000..e69de29b diff --git a/.kokoro/release/docs-devsite.cfg b/.kokoro/release/docs-devsite.cfg index 3446a019..fefc8723 100644 --- a/.kokoro/release/docs-devsite.cfg +++ b/.kokoro/release/docs-devsite.cfg @@ -11,7 +11,7 @@ before_action { # doc publications use a Python image. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } # Download trampoline resources. diff --git a/.kokoro/release/docs.cfg b/.kokoro/release/docs.cfg index 54d2a854..d424347d 100644 --- a/.kokoro/release/docs.cfg +++ b/.kokoro/release/docs.cfg @@ -11,7 +11,7 @@ before_action { # doc publications use a Python image. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } # Download trampoline resources. diff --git a/.kokoro/release/docs.sh b/.kokoro/release/docs.sh index 1d8f3f49..e9079a60 100755 --- a/.kokoro/release/docs.sh +++ b/.kokoro/release/docs.sh @@ -16,7 +16,7 @@ set -eo pipefail -# build jsdocs (Python is installed on the Node 10 docker image). +# build jsdocs (Python is installed on the Node 18 docker image). if [[ -z "$CREDENTIALS" ]]; then # if CREDENTIALS are explicitly set, assume we're testing locally # and don't set NPM_CONFIG_PREFIX. diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index 8626c14d..9ea0570f 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -30,7 +30,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } env_vars: { diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index 8c5d108c..52877539 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -16,7 +16,9 @@ set -eo pipefail -export NPM_CONFIG_PREFIX=${HOME}/.npm-global +# Ensure the npm global directory is writable, otherwise rebuild `npm` +mkdir -p $NPM_CONFIG_PREFIX +npm config -g ls || npm i -g npm@`npm --version` # Setup service account credentials. export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/secret_manager/long-door-651-kokoro-system-test-service-account @@ -56,7 +58,7 @@ fi # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=14 +COVERAGE_NODE=18 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 0b3043d2..a90d5cfe 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -49,7 +49,7 @@ npm run system-test # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=14 +COVERAGE_NODE=18 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/test.bat b/.kokoro/test.bat index 0bb12405..caf82565 100644 --- a/.kokoro/test.bat +++ b/.kokoro/test.bat @@ -21,7 +21,7 @@ cd .. @rem we upgrade Node.js in the image: SET PATH=%PATH%;/cygdrive/c/Program Files/nodejs/npm -call nvm use v14.17.3 +call nvm use 18 call which node call npm install || goto :error diff --git a/.kokoro/test.sh b/.kokoro/test.sh index 862d478d..0d9f6392 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -39,7 +39,7 @@ npm test # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=14 +COVERAGE_NODE=18 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh index 4d031121..5d6cfcca 100755 --- a/.kokoro/trampoline_v2.sh +++ b/.kokoro/trampoline_v2.sh @@ -44,7 +44,7 @@ # the project root. # # Here is an example for running this script. -# TRAMPOLINE_IMAGE=gcr.io/cloud-devrel-kokoro-resources/node:10-user \ +# TRAMPOLINE_IMAGE=gcr.io/cloud-devrel-kokoro-resources/node:18-user \ # TRAMPOLINE_BUILD_FILE=.kokoro/system-test.sh \ # .kokoro/trampoline_v2.sh diff --git a/README.md b/README.md index 441c2748..65b5b138 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ This library provides a variety of ways to authenticate to your Google services. - [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential Access Boundary to generate a short-lived credential with downscoped, restricted IAM permissions that can use for Cloud Storage. ## Application Default Credentials + This library provides an implementation of [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials) for Node.js. ADC provides a simple way to get credentials for use in calling Google APIs. How you [set up ADC](https://cloud.google.com/docs/authentication/provide-credentials-adc) depends on the environment where your code is running. ADC is best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Cloud Platform. From 8d3bbc4a6275d13c47872d2568f42c33ae88732e Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 3 Feb 2025 20:15:23 +0100 Subject: [PATCH 576/662] chore(deps): update dependency @types/node to v22 (#1888) --- package.json | 2 +- system-test/fixtures/kitchen/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a7e3231e..b24d64db 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@types/mocha": "^9.0.0", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", - "@types/node": "^20.4.2", + "@types/node": "^22.0.0", "@types/sinon": "^17.0.0", "assert-rejects": "^1.0.0", "c8": "^8.0.0", diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index ecf79ef9..e075d89f 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -17,7 +17,7 @@ "google-auth-library": "file:./google-auth-library.tgz" }, "devDependencies": { - "@types/node": "^20.0.0", + "@types/node": "^22.0.0", "typescript": "^5.0.0", "gts": "^5.0.0", "null-loader": "^4.0.0", From c4713768a207021c4f5cc9f30a93eefadf6425a6 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 3 Feb 2025 20:17:33 +0100 Subject: [PATCH 577/662] chore(deps): update actions/checkout digest to 11bd719 (#1887) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7a90c6cf..935dccc8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [14, 16, 18, 20] steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-node@v4 with: node-version: 14 @@ -40,7 +40,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-node@v4 with: node-version: 14 From 974d67a59db994286cf6ca3be9baa9d978b6c4eb Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:46:44 -0800 Subject: [PATCH 578/662] test: Skip webpack v4 test (#1930) Its finicky, and requires constant config changes based on upstream dependencies. --- system-test/test.kitchen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index da566d1d..5fa53f67 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -97,7 +97,7 @@ describe('pack and install', () => { await packAndInstall(); }); - it('should be able to webpack the library', async function () { + it.skip('should be able to webpack the library', async function () { this.retries(3); this.timeout(BUILD_TEST_TIMEOUT_MS); From 5b60a95d1759fcd4a3a3614e8345203a4e1d29f2 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:20:12 -0800 Subject: [PATCH 579/662] feat!: Support Node 18, 20, and 22 (#1928) * feat: Support Node 18, 20, & 22 * chore: dep clean-up --- .github/sync-repo-settings.yaml | 4 ++-- .github/workflows/ci.yaml | 6 +++--- package.json | 7 ++----- samples/package.json | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index b46e4c4d..a013376d 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -8,9 +8,9 @@ branchProtectionRules: - "ci/kokoro: Samples test" - "ci/kokoro: System test" - lint - - test (14) - - test (16) - test (18) + - test (20) + - test (22) - cla/google - windows - OwlBot Post Processor diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 935dccc8..a67c3ec2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [14, 16, 18, 20] + node: [18, 20, 22] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-node@v4 @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-node@v4 with: - node-version: 14 + node-version: 18 - run: npm install - run: npm test env: @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-node@v4 with: - node-version: 14 + node-version: 18 - run: npm install - run: npm run lint docs: diff --git a/package.json b/package.json index b24d64db..12274350 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { - "node": ">=14" + "node": ">=18" }, "main": "./build/src/index.js", "types": "./build/src/index.d.ts", @@ -36,9 +36,7 @@ "assert-rejects": "^1.0.0", "c8": "^8.0.0", "chai": "^4.2.0", - "cheerio": "1.0.0-rc.12", "codecov": "^3.0.2", - "engine.io": "6.6.2", "gts": "^5.0.0", "is-docker": "^2.0.0", "jsdoc": "^4.0.0", @@ -52,13 +50,12 @@ "karma-sourcemap-loader": "^0.4.0", "karma-webpack": "5.0.0", "keypair": "^1.0.4", - "linkinator": "^4.0.0", + "linkinator": "^6.1.2", "mocha": "^9.2.2", "mv": "^2.1.1", "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "pdfmake": "0.2.12", "puppeteer": "^21.0.0", "sinon": "^18.0.0", "ts-loader": "^8.0.0", diff --git a/samples/package.json b/samples/package.json index 87dde05f..02fdde5d 100644 --- a/samples/package.json +++ b/samples/package.json @@ -9,7 +9,7 @@ "test": "mocha --timeout 60000" }, "engines": { - "node": ">=14" + "node": ">=18" }, "license": "Apache-2.0", "dependencies": { From 3d240453d48a092db4b43c6f000751160ef1dea4 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:20:56 -0800 Subject: [PATCH 580/662] feat!: Support Node 18+ (#1879) * chore: Support Node 18+ * feat: Support Node 18+ * chore: Additional Node 18 upgrades * chore: don't change test fixtures * feat: Move kokoro directories to 18 * chore: Update checkout and setup-node to v4 - https://github.com/actions/checkout/releases/tag/v4.0.0 - https://github.com/actions/setup-node/releases/tag/v4.0.0 Source-Link: https://github.com/googleapis/synthtool/commit/c19dd80df72683437f79151b746c2f22c6bdf8b7 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:8060aba1f6d5617d08091767141ab2a99ea1ccbd9371fd42ffc208c5329caa73 * chore: Update `engines` and minor functionality --------- Co-authored-by: Owl Bot Co-authored-by: Daniel Bankhead Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- .kokoro/continuous/node14/common.cfg | 2 +- .kokoro/presubmit/node14/common.cfg | 2 +- samples/puppeteer/package.json | 2 +- src/messages.ts | 6 +----- system-test/test.kitchen.ts | 17 ++++------------- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/.kokoro/continuous/node14/common.cfg b/.kokoro/continuous/node14/common.cfg index eb5e5c10..16c6b60e 100644 --- a/.kokoro/continuous/node14/common.cfg +++ b/.kokoro/continuous/node14/common.cfg @@ -16,7 +16,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/presubmit/node14/common.cfg b/.kokoro/presubmit/node14/common.cfg index eb5e5c10..16c6b60e 100644 --- a/.kokoro/presubmit/node14/common.cfg +++ b/.kokoro/presubmit/node14/common.cfg @@ -16,7 +16,7 @@ build_file: "google-auth-library-nodejs/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index ced88955..bff1f55f 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -4,7 +4,7 @@ "description": "An example of using puppeteer to orchestrate a Google sign in flow.", "main": "oauth2-test.js", "engines": { - "node": ">=12" + "node": ">=18" }, "scripts": { "start": "node oauth2-test.js" diff --git a/src/messages.ts b/src/messages.ts index ccfdf083..62c382b0 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -24,11 +24,7 @@ export function warn(warning: Warning) { } warning.warned = true; if (typeof process !== 'undefined' && process.emitWarning) { - // @types/node doesn't recognize the emitWarning syntax which - // accepts a config object, so `as any` it is - // https://nodejs.org/docs/latest-v8.x/api/process.html#process_process_emitwarning_warning_options - // eslint-disable-next-line @typescript-eslint/no-explicit-any - process.emitWarning(warning.message, warning as any); + process.emitWarning(warning.message, warning); } else { console.warn(warning.message); } diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 5fa53f67..1eefd737 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -114,19 +114,10 @@ describe('pack and install', () => { */ afterEach('cleanup staging', async () => { if (!keep) { - if ('rm' in fs.promises) { - await fs.promises.rm(stagingDir, { - force: true, - recursive: true, - }); - } else { - // Must be on Node 14-. - // Here, `rmdir` can also delete files. - // Background: https://github.com/nodejs/node/issues/34278 - await fs.promises.rmdir(stagingDir, { - recursive: true, - }); - } + await fs.promises.rm(stagingDir, { + force: true, + recursive: true, + }); } }); }); From 654753dc6a85bfefeee4d3c87439183deb13212d Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:23:00 -0800 Subject: [PATCH 581/662] refactor!: Remove `messages.ts` (#1919) --- src/messages.ts | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 src/messages.ts diff --git a/src/messages.ts b/src/messages.ts deleted file mode 100644 index 62c382b0..00000000 --- a/src/messages.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2018 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. - -export enum WarningTypes { - WARNING = 'Warning', - DEPRECATION = 'DeprecationWarning', -} - -export function warn(warning: Warning) { - // Only show a given warning once - if (warning.warned) { - return; - } - warning.warned = true; - if (typeof process !== 'undefined' && process.emitWarning) { - process.emitWarning(warning.message, warning); - } else { - console.warn(warning.message); - } -} - -export interface Warning { - code: string; - type: WarningTypes; - message: string; - warned?: boolean; -} From 51316e8e75f111b897b284cc77d8429e4db8e25a Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:24:11 -0800 Subject: [PATCH 582/662] refactor!: Remove `options.ts` (#1920) * refactor!: Remove `options.ts` * chore: clean-up * test: remove unnecessary test --- src/options.ts | 32 -------------------------------- src/transporters.ts | 3 --- test/test.transporters.ts | 10 ---------- 3 files changed, 45 deletions(-) delete mode 100644 src/options.ts diff --git a/src/options.ts b/src/options.ts deleted file mode 100644 index bda11d9f..00000000 --- a/src/options.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2017 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. - -// Accepts an options object passed from the user to the API. In the -// previous version of the API, it referred to a `Request` options object. -// Now it refers to an Axiox Request Config object. This is here to help -// ensure users don't pass invalid options when they upgrade from 0.x to 1.x. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function validate(options: any) { - const vpairs = [ - {invalid: 'uri', expected: 'url'}, - {invalid: 'json', expected: 'data'}, - {invalid: 'qs', expected: 'params'}, - ]; - for (const pair of vpairs) { - if (options[pair.invalid]) { - const e = `'${pair.invalid}' is not a valid configuration option. Please use '${pair.expected}' instead. This library is using Axios for requests. Please see https://github.com/axios/axios to learn more about the valid request options.`; - throw new Error(e); - } - } -} diff --git a/src/transporters.ts b/src/transporters.ts index 41660308..c60a556d 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -19,9 +19,7 @@ import { GaxiosPromise, GaxiosResponse, } from 'gaxios'; -import {validate} from './options'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../package.json'); const PRODUCT_NAME = 'google-api-nodejs-client'; @@ -85,7 +83,6 @@ export class DefaultTransporter implements Transporter { request(opts: GaxiosOptions): GaxiosPromise { // ensure the user isn't passing in request-style options opts = this.configure(opts); - validate(opts); return this.instance.request(opts).catch(e => { throw this.processError(e); }); diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 6c019191..055ede56 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -121,16 +121,6 @@ describe('transporters', () => { ); }); - it('should return an error if you try to use request config options with a promise', async () => { - const expected = new RegExp( - "'uri' is not a valid configuration option. Please use 'url' instead. This " + - 'library is using Axios for requests. Please see https://github.com/axios/axios ' + - 'to learn more about the valid request options.' - ); - const uri = 'http://example.com/api'; - assert.throws(() => transporter.request({uri} as GaxiosOptions), expected); - }); - it('should support invocation with async/await', async () => { const url = 'http://example.com'; const scope = nock(url).get('/').reply(200); From 2f780a85e11fe2cfb0dbf7f91dfbd90d15207491 Mon Sep 17 00:00:00 2001 From: Aldrin <53973174+Dhoni77@users.noreply.github.com> Date: Wed, 5 Feb 2025 07:19:15 +0530 Subject: [PATCH 583/662] refactor!: remove additionalOptions from AuthClients (#1689) * chore: remove additionalOptions from clients and quotaProjectId param from DownScopedClient * refactor: update unit tests * chore: fix formatting * test: minor refactor * docs: Update `DownscopedClient` guidance * refactor!: Streamline JWT parameters * chore: clean-up * refactor!: Streamline `OAuth2Client` Constructor * refactor!: Streamline `UserRefreshClient` Construction --------- Co-authored-by: Daniel Bankhead --- .readme-partials.yaml | 12 +-- samples/downscopedclient.js | 7 +- samples/oauth2-codeVerifier.js | 6 +- samples/oauth2.js | 6 +- samples/puppeteer/oauth2-test.js | 10 +- samples/verifyIdToken.js | 10 +- src/auth/awsclient.ts | 10 +- src/auth/baseexternalclient.ts | 9 +- src/auth/downscopedclient.ts | 55 ++++++++--- .../externalAccountAuthorizedUserClient.ts | 20 ++-- src/auth/externalclient.ts | 20 +--- src/auth/googleauth.ts | 16 +-- src/auth/identitypoolclient.ts | 10 +- src/auth/jwtclient.ts | 71 ++++++------- src/auth/oauth2client.ts | 97 +++++++++++++----- src/auth/pluggable-auth-client.ts | 12 +-- src/auth/refreshclient.ts | 42 +++++--- test/test.baseexternalclient.ts | 41 ++++---- test/test.downscopedclient.ts | 59 +++++------ ...est.externalaccountauthorizeduserclient.ts | 38 +++---- test/test.externalclient.ts | 33 ++++--- test/test.googleauth.ts | 99 ++++++++----------- test/test.impersonated.ts | 23 +++-- test/test.jwt.ts | 13 ++- 24 files changed, 365 insertions(+), 354 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 3590f619..23c91acf 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -103,11 +103,11 @@ body: |- return new Promise((resolve, reject) => { // create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file, // which should be downloaded from the Google Developers Console. - const oAuth2Client = new OAuth2Client( - keys.web.client_id, - keys.web.client_secret, - keys.web.redirect_uris[0] - ); + const oAuth2Client = new OAuth2Client({ + clientId: keys.web.client_id, + clientSecret: keys.web.client_secret, + redirectUri: keys.web.redirect_uris[0] + }); // Generate the url that will be used for the consent dialog. const authorizeUrl = oAuth2Client.generateAuthUrl({ @@ -1260,7 +1260,7 @@ body: |- const client = await googleAuth.getClient(); // Use the client to create a DownscopedClient. - const cabClient = new DownscopedClient(client, cab); + const cabClient = new DownscopedClient({authClient: client, credentialAccessBoundary: cab}); // Refresh the tokens. const refreshedAccessToken = await cabClient.getAccessToken(); diff --git a/samples/downscopedclient.js b/samples/downscopedclient.js index c33d51d5..fd9607b5 100644 --- a/samples/downscopedclient.js +++ b/samples/downscopedclient.js @@ -38,7 +38,7 @@ async function main() { const objectName = process.env.OBJECT_NAME; // Defines a credential access boundary that grants objectViewer access in // the specified bucket. - const cab = { + const credentialAccessBoundary = { accessBoundary: { accessBoundaryRules: [ { @@ -61,7 +61,10 @@ async function main() { // Obtain an authenticated client via ADC. const client = await googleAuth.getClient(); // Use the client to generate a DownscopedClient. - const cabClient = new DownscopedClient(client, cab); + const cabClient = new DownscopedClient({ + authClient: client, + credentialAccessBoundary, + }); // OAuth 2.0 Client const authClient = new OAuth2Client(); diff --git a/samples/oauth2-codeVerifier.js b/samples/oauth2-codeVerifier.js index 37065042..4436ed10 100644 --- a/samples/oauth2-codeVerifier.js +++ b/samples/oauth2-codeVerifier.js @@ -46,11 +46,7 @@ async function getAuthenticatedClient() { // create an oAuth client to authorize the API call. Secrets are kept in a // `keys.json` file, which should be downloaded from the Google Developers // Console. - const oAuth2Client = new OAuth2Client( - keys.web.client_id, - keys.web.client_secret, - keys.web.redirect_uris[0] - ); + const oAuth2Client = new OAuth2Client(keys.web); // Generate a code_verifier and code_challenge const codes = await oAuth2Client.generateCodeVerifierAsync(); diff --git a/samples/oauth2.js b/samples/oauth2.js index a10fab2b..cabf1e2d 100644 --- a/samples/oauth2.js +++ b/samples/oauth2.js @@ -53,11 +53,7 @@ function getAuthenticatedClient() { return new Promise((resolve, reject) => { // create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file, // which should be downloaded from the Google Developers Console. - const oAuth2Client = new OAuth2Client( - keys.web.client_id, - keys.web.client_secret, - keys.web.redirect_uris[0] - ); + const oAuth2Client = new OAuth2Client(keys.web); // Generate the url that will be used for the consent dialog. const authorizeUrl = oAuth2Client.generateAuthUrl({ diff --git a/samples/puppeteer/oauth2-test.js b/samples/puppeteer/oauth2-test.js index 71fa5908..49fcadb4 100644 --- a/samples/puppeteer/oauth2-test.js +++ b/samples/puppeteer/oauth2-test.js @@ -53,11 +53,11 @@ function getAuthenticatedClient() { return new Promise(resolve => { // create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file, // which should be downloaded from the Google Developers Console. - const oAuth2Client = new OAuth2Client( - keys.web.client_id, - keys.web.client_secret, - keys.web.redirect_uris[0] - ); + const oAuth2Client = new OAuth2Client({ + clientId: keys.web.client_id, + clientSecret: keys.web.client_secret, + redirectUri: keys.web.redirect_uris[0], + }); // Generate the url that will be used for the consent dialog. const authorizeUrl = oAuth2Client.generateAuthUrl({ diff --git a/samples/verifyIdToken.js b/samples/verifyIdToken.js index bb9f0dc1..c4bc35dc 100644 --- a/samples/verifyIdToken.js +++ b/samples/verifyIdToken.js @@ -53,11 +53,11 @@ function getAuthenticatedClient() { return new Promise((resolve, reject) => { // create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file, // which should be downloaded from the Google Developers Console. - const oAuth2Client = new OAuth2Client( - keys.web.client_id, - keys.web.client_secret, - keys.web.redirect_uris[0] - ); + const oAuth2Client = new OAuth2Client({ + clientId: keys.web.client_id, + clientSecret: keys.web.client_secret, + redirectUri: keys.web.redirect_uris[0], + }); // Generate the url that will be used for the consent dialog. const authorizeUrl = oAuth2Client.generateAuthUrl({ diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index c3fadf68..cb296143 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -19,7 +19,6 @@ import { ExternalAccountSupplierContext, } from './baseexternalclient'; -import {AuthClientOptions} from './authclient'; import {DefaultAwsSecurityCredentialsSupplier} from './defaultawssecuritycredentialssupplier'; import {originalOrCamelOptions, SnakeToCamelObject} from '../util'; @@ -131,16 +130,11 @@ export class AwsClient extends BaseExternalAccountClient { * An error is thrown if the credential is not a valid AWS credential. * @param options The external account options object typically loaded * from the external account JSON credential file. - * @param additionalOptions **DEPRECATED, all options are available in the - * `options` parameter.** Optional additional behavior customization options. - * These currently customize expiration threshold time and whether to retry - * on 401/403 API request errors. */ constructor( - options: AwsClientOptions | SnakeToCamelObject, - additionalOptions?: AuthClientOptions + options: AwsClientOptions | SnakeToCamelObject ) { - super(options, additionalOptions); + super(options); const opts = originalOrCamelOptions(options as AwsClientOptions); const credentialSource = opts.get('credential_source'); const awsSecurityCredentialsSupplier = opts.get( diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 6f480979..5d91a407 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -262,18 +262,13 @@ export abstract class BaseExternalAccountClient extends AuthClient { * @param options The external account options object typically loaded * from the external account JSON credential file. The camelCased options * are aliases for the snake_cased options. - * @param additionalOptions **DEPRECATED, all options are available in the - * `options` parameter.** Optional additional behavior customization options. - * These currently customize expiration threshold time and whether to retry - * on 401/403 API request errors. */ constructor( options: | BaseExternalAccountClientOptions - | SnakeToCamelObject, - additionalOptions?: AuthClientOptions + | SnakeToCamelObject ) { - super({...options, ...additionalOptions}); + super(options); const opts = originalOrCamelOptions( options as BaseExternalAccountClientOptions diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index d4338f0b..5d9bbc70 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -96,6 +96,20 @@ interface AvailabilityCondition { description?: string; } +export interface DownscopedClientOptions extends AuthClientOptions { + /** + * The source AuthClient to be downscoped based on the provided Credential Access Boundary rules. + */ + authClient: AuthClient; + /** + * The Credential Access Boundary which contains a list of access boundary rules. + * Each rule contains information on the resource that the rule applies to, the upper bound of the + * permissions that are available on that resource and an optional + * condition to further restrict permissions. + */ + credentialAccessBoundary: CredentialAccessBoundary; +} + /** * Defines a set of Google credentials that are downscoped from an existing set * of Google OAuth2 credentials. This is useful to restrict the Identity and @@ -107,6 +121,8 @@ interface AvailabilityCondition { * resources. */ export class DownscopedClient extends AuthClient { + private readonly authClient: AuthClient; + private readonly credentialAccessBoundary: CredentialAccessBoundary; private cachedDownscopedAccessToken: CredentialsWithResponse | null; private readonly stsCredential: sts.StsCredentials; @@ -118,25 +134,32 @@ export class DownscopedClient extends AuthClient { * well as an upper bound on the permissions that are available on each * resource, has to be defined. A downscoped client can then be instantiated * using the source AuthClient and the Credential Access Boundary. - * @param authClient The source AuthClient to be downscoped based on the - * provided Credential Access Boundary rules. - * @param credentialAccessBoundary The Credential Access Boundary which - * contains a list of access boundary rules. Each rule contains information - * on the resource that the rule applies to, the upper bound of the - * permissions that are available on that resource and an optional - * condition to further restrict permissions. - * @param additionalOptions **DEPRECATED, set this in the provided `authClient`.** - * Optional additional behavior customization options. - * @param quotaProjectId **DEPRECATED, set this in the provided `authClient`.** - * Optional quota project id for setting up in the x-goog-user-project header. + * @param options the {@link DownscopedClientOptions `DownscopedClientOptions`} to use. Passing an `AuthClient` directly is **@DEPRECATED**. + * @param credentialAccessBoundary **@DEPRECATED**. Provide a {@link DownscopedClientOptions `DownscopedClientOptions`} object in the first parameter instead. */ constructor( - private readonly authClient: AuthClient, - private readonly credentialAccessBoundary: CredentialAccessBoundary, - additionalOptions?: AuthClientOptions, - quotaProjectId?: string + /** + * AuthClient is for backwards-compatibility. + */ + options: AuthClient | DownscopedClientOptions, + /** + * @deprecated - provide a {@link DownscopedClientOptions `DownscopedClientOptions`} object in the first parameter instead + */ + credentialAccessBoundary: CredentialAccessBoundary = { + accessBoundary: { + accessBoundaryRules: [], + }, + } ) { - super({...additionalOptions, quotaProjectId}); + super(options instanceof AuthClient ? {} : options); + + if (options instanceof AuthClient) { + this.authClient = options; + this.credentialAccessBoundary = credentialAccessBoundary; + } else { + this.authClient = options.authClient; + this.credentialAccessBoundary = options.credentialAccessBoundary; + } // Check 1-10 Access Boundary Rules are defined within Credential Access // Boundary. diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index 29812c4b..3cbef1fd 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {AuthClient, AuthClientOptions, Headers} from './authclient'; +import {AuthClient, Headers} from './authclient'; import { ClientAuthentication, getErrorFromOAuthErrorResponse, @@ -162,16 +162,9 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { * An error is throws if the credential is not valid. * @param options The external account authorized user option object typically * from the external accoutn authorized user JSON credential file. - * @param additionalOptions **DEPRECATED, all options are available in the - * `options` parameter.** Optional additional behavior customization options. - * These currently customize expiration threshold time and whether to retry - * on 401/403 API request errors. */ - constructor( - options: ExternalAccountAuthorizedUserClientOptions, - additionalOptions?: AuthClientOptions - ) { - super({...options, ...additionalOptions}); + constructor(options: ExternalAccountAuthorizedUserClientOptions) { + super(options); if (options.universe_domain) { this.universeDomain = options.universe_domain; } @@ -195,13 +188,14 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { // As threshold could be zero, // eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the // zero value. - if (typeof additionalOptions?.eagerRefreshThresholdMillis !== 'number') { + if (typeof options?.eagerRefreshThresholdMillis !== 'number') { this.eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET; } else { - this.eagerRefreshThresholdMillis = additionalOptions! + this.eagerRefreshThresholdMillis = options! .eagerRefreshThresholdMillis as number; } - this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure; + + this.forceRefreshOnFailure = !!options?.forceRefreshOnFailure; } async getAccessToken(): Promise<{ diff --git a/src/auth/externalclient.ts b/src/auth/externalclient.ts index 95b97da3..635f4fd6 100644 --- a/src/auth/externalclient.ts +++ b/src/auth/externalclient.ts @@ -32,7 +32,6 @@ import { PluggableAuthClient, PluggableAuthClientOptions, } from './pluggable-auth-client'; -import {AuthClientOptions} from './authclient'; export type ExternalAccountClientOptions = | IdentityPoolClientOptions @@ -60,32 +59,21 @@ export class ExternalAccountClient { * underlying credential source. * @param options The external account options object typically loaded * from the external account JSON credential file. - * @param additionalOptions **DEPRECATED, all options are available in the - * `options` parameter.** Optional additional behavior customization options. - * These currently customize expiration threshold time and whether to retry - * on 401/403 API request errors. * @return A BaseExternalAccountClient instance or null if the options * provided do not correspond to an external account credential. */ static fromJSON( - options: ExternalAccountClientOptions, - additionalOptions?: AuthClientOptions + options: ExternalAccountClientOptions ): BaseExternalAccountClient | null { if (options && options.type === EXTERNAL_ACCOUNT_TYPE) { if ((options as AwsClientOptions).credential_source?.environment_id) { - return new AwsClient(options as AwsClientOptions, additionalOptions); + return new AwsClient(options as AwsClientOptions); } else if ( (options as PluggableAuthClientOptions).credential_source?.executable ) { - return new PluggableAuthClient( - options as PluggableAuthClientOptions, - additionalOptions - ); + return new PluggableAuthClient(options as PluggableAuthClientOptions); } else { - return new IdentityPoolClient( - options as IdentityPoolClientOptions, - additionalOptions - ); + return new IdentityPoolClient(options as IdentityPoolClientOptions); } } else { return null; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index e70e186e..54c0c312 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -692,16 +692,16 @@ export class GoogleAuth { } else if (json.type === IMPERSONATED_ACCOUNT_TYPE) { client = this.fromImpersonatedJSON(json as ImpersonatedJWTInput); } else if (json.type === EXTERNAL_ACCOUNT_TYPE) { - client = ExternalAccountClient.fromJSON( - json as ExternalAccountClientOptions, - options - )!; + client = ExternalAccountClient.fromJSON({ + ...json, + ...options, + } as ExternalAccountClientOptions)!; client.scopes = this.getAnyScopes(); } else if (json.type === EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE) { - client = new ExternalAccountAuthorizedUserClient( - json as ExternalAccountAuthorizedUserClientOptions, - options - ); + client = new ExternalAccountAuthorizedUserClient({ + ...json, + ...options, + } as ExternalAccountAuthorizedUserClientOptions); } else { (options as JWTOptions).scopes = this.scopes; client = new JWT(options); diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index 160dce03..fa2c1d00 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -17,7 +17,6 @@ import { BaseExternalAccountClientOptions, ExternalAccountSupplierContext, } from './baseexternalclient'; -import {AuthClientOptions} from './authclient'; import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; import {FileSubjectTokenSupplier} from './filesubjecttokensupplier'; import {UrlSubjectTokenSupplier} from './urlsubjecttokensupplier'; @@ -113,18 +112,13 @@ export class IdentityPoolClient extends BaseExternalAccountClient { * @param options The external account options object typically loaded * from the external account JSON credential file. The camelCased options * are aliases for the snake_cased options. - * @param additionalOptions **DEPRECATED, all options are available in the - * `options` parameter.** Optional additional behavior customization options. - * These currently customize expiration threshold time and whether to retry - * on 401/403 API request errors. */ constructor( options: | IdentityPoolClientOptions - | SnakeToCamelObject, - additionalOptions?: AuthClientOptions + | SnakeToCamelObject ) { - super(options, additionalOptions); + super(options); const opts = originalOrCamelOptions(options as IdentityPoolClientOptions); const credentialSource = opts.get('credential_source'); diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 76fe00f7..8ba1fd0c 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -27,12 +27,38 @@ import { import {DEFAULT_UNIVERSE} from './authclient'; export interface JWTOptions extends OAuth2ClientOptions { + /** + * The service account email address. + */ email?: string; + /** + * The path to private key file. Not necessary if {@link JWTOptions.key} has been provided. + */ keyFile?: string; + /** + * The value of key. Not necessary if {@link JWTOptions.keyFile} has been provided. + */ key?: string; + /** + * The list of requested scopes or a single scope. + */ keyId?: string; + /** + * The impersonated account's email address. + */ scopes?: string | string[]; + /** + * The ID of the key. + */ subject?: string; + /** + * Additional claims, such as target audience. + * + * @example + * ``` + * {target_audience: 'targetAudience'} + * ``` + */ additionalClaims?: {}; } @@ -56,42 +82,17 @@ export class JWT extends OAuth2Client implements IdTokenProvider { * * Retrieve access token using gtoken. * - * @param email service account email address. - * @param keyFile path to private key file. - * @param key value of key - * @param scopes list of requested scopes or a single scope. - * @param subject impersonated account's email address. - * @param key_id the ID of the key + * @param options the */ - constructor(options: JWTOptions); - constructor( - email?: string, - keyFile?: string, - key?: string, - scopes?: string | string[], - subject?: string, - keyId?: string - ); - constructor( - optionsOrEmail?: string | JWTOptions, - keyFile?: string, - key?: string, - scopes?: string | string[], - subject?: string, - keyId?: string - ) { - const opts = - optionsOrEmail && typeof optionsOrEmail === 'object' - ? optionsOrEmail - : {email: optionsOrEmail, keyFile, key, keyId, scopes, subject}; - super(opts); - this.email = opts.email; - this.keyFile = opts.keyFile; - this.key = opts.key; - this.keyId = opts.keyId; - this.scopes = opts.scopes; - this.subject = opts.subject; - this.additionalClaims = opts.additionalClaims; + constructor(options: JWTOptions = {}) { + super(options); + this.email = options.email; + this.keyFile = options.keyFile; + this.key = options.key; + this.keyId = options.keyId; + this.scopes = options.scopes; + this.subject = options.subject; + this.additionalClaims = options.additionalClaims; // Start with an expired refresh token, which will automatically be // refreshed before the first API call is made. this.credentials = {refresh_token: 'jwt-placeholder', expiry_date: 1}; diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index e9af50c2..f74720ce 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -33,6 +33,7 @@ import { } from './authclient'; import {CredentialRequest, Credentials} from './credentials'; import {LoginTicket, TokenPayload} from './loginticket'; + /** * The results from the `generateCodeVerifierAsync` method. To learn more, * See the sample: @@ -482,9 +483,51 @@ export interface OAuth2ClientEndpoints { oauth2IapPublicKeyUrl: string | URL; } -export interface OAuth2ClientOptions extends AuthClientOptions { +/** + * A convenient interface for those looking to pass the OAuth2 Client config via a parsed + * JSON file. + */ +interface OAuth2JSONOptions { + /** + * The authentication client ID. + * + * @alias {@link OAuth2ClientOptions.clientId} + */ + client_id?: string; + /** + * The authentication client secret. + * + * @alias {@link OAuth2ClientOptions.clientSecret} + */ + client_secret?: string; + /** + * The URIs to redirect to after completing the auth request. + * + * @alias {@link OAuth2ClientOptions.redirectUri} + */ + redirect_uris?: string[]; +} + +export interface OAuth2ClientOptions + extends AuthClientOptions, + OAuth2JSONOptions { + /** + * The authentication client ID. + * + * @alias {@link OAuth2JSONOptions.client_id} + */ clientId?: string; + /** + * The authentication client secret. + * + * @alias {@link OAuth2JSONOptions.client_secret} + */ clientSecret?: string; + /** + * The URI to redirect to after completing the auth request. + * + * @alias {@link OAuth2JSONOptions.redirect_uris} + */ redirectUri?: string; /** * Customizable endpoints. @@ -527,32 +570,36 @@ export class OAuth2Client extends AuthClient { refreshHandler?: GetRefreshHandlerCallback; /** - * Handles OAuth2 flow for Google APIs. + * An OAuth2 Client for Google APIs. * - * @param clientId The authentication client ID. - * @param clientSecret The authentication client secret. - * @param redirectUri The URI to redirect to after completing the auth - * request. - * @param opts optional options for overriding the given parameters. - * @constructor - */ - constructor(options?: OAuth2ClientOptions); - constructor(clientId?: string, clientSecret?: string, redirectUri?: string); + * @param options The OAuth2 Client Options. Passing an `clientId` directly is **@DEPRECATED**. + * @param clientSecret **@DEPRECATED**. Provide a {@link OAuth2ClientOptions `OAuth2ClientOptions`} object in the first parameter instead. + * @param redirectUri **@DEPRECATED**. Provide a {@link OAuth2ClientOptions `OAuth2ClientOptions`} object in the first parameter instead. + */ constructor( - optionsOrClientId?: string | OAuth2ClientOptions, - clientSecret?: string, - redirectUri?: string + options: OAuth2ClientOptions | OAuth2ClientOptions['clientId'] = {}, + /** + * @deprecated - provide a {@link OAuth2ClientOptions `OAuth2ClientOptions`} object in the first parameter instead + */ + clientSecret?: OAuth2ClientOptions['clientSecret'], + /** + * @deprecated - provide a {@link OAuth2ClientOptions `OAuth2ClientOptions`} object in the first parameter instead + */ + redirectUri?: OAuth2ClientOptions['redirectUri'] ) { - const opts = - optionsOrClientId && typeof optionsOrClientId === 'object' - ? optionsOrClientId - : {clientId: optionsOrClientId, clientSecret, redirectUri}; + super(typeof options === 'object' ? options : {}); - super(opts); + if (typeof options !== 'object') { + options = { + clientId: options, + clientSecret, + redirectUri, + }; + } - this._clientId = opts.clientId; - this._clientSecret = opts.clientSecret; - this.redirectUri = opts.redirectUri; + this._clientId = options.clientId || options.client_id; + this._clientSecret = options.clientSecret || options.client_secret; + this.redirectUri = options.redirectUri || options.redirect_uris?.[0]; this.endpoints = { tokenInfoUrl: 'https://oauth2.googleapis.com/tokeninfo', @@ -564,12 +611,12 @@ export class OAuth2Client extends AuthClient { oauth2FederatedSignonJwkCertsUrl: 'https://www.googleapis.com/oauth2/v3/certs', oauth2IapPublicKeyUrl: 'https://www.gstatic.com/iap/verify/public_key', - ...opts.endpoints, + ...options.endpoints, }; this.clientAuthentication = - opts.clientAuthentication || ClientAuthentication.ClientSecretPost; + options.clientAuthentication || ClientAuthentication.ClientSecretPost; - this.issuers = opts.issuers || [ + this.issuers = options.issuers || [ 'accounts.google.com', 'https://accounts.google.com', this.universeDomain, diff --git a/src/auth/pluggable-auth-client.ts b/src/auth/pluggable-auth-client.ts index ad9a933f..ad9bbd23 100644 --- a/src/auth/pluggable-auth-client.ts +++ b/src/auth/pluggable-auth-client.ts @@ -21,7 +21,6 @@ import { InvalidExpirationTimeFieldError, } from './executable-response'; import {PluggableAuthHandler} from './pluggable-auth-handler'; -import {AuthClientOptions} from './authclient'; /** * Defines the credential source portion of the configuration for PluggableAuthClient. @@ -189,16 +188,9 @@ export class PluggableAuthClient extends BaseExternalAccountClient { * An error is thrown if the credential is not a valid pluggable auth credential. * @param options The external account options object typically loaded from * the external account JSON credential file. - * @param additionalOptions **DEPRECATED, all options are available in the - * `options` parameter.** Optional additional behavior customization options. - * These currently customize expiration threshold time and whether to retry - * on 401/403 API request errors. */ - constructor( - options: PluggableAuthClientOptions, - additionalOptions?: AuthClientOptions - ) { - super(options, additionalOptions); + constructor(options: PluggableAuthClientOptions) { + super(options); if (!options.credential_source.executable) { throw new Error('No valid Pluggable Auth "credential_source" provided.'); } diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 93c07d49..525c5412 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -24,8 +24,17 @@ import {stringify} from 'querystring'; export const USER_REFRESH_ACCOUNT_TYPE = 'authorized_user'; export interface UserRefreshClientOptions extends OAuth2ClientOptions { + /** + * The authentication client ID. + */ clientId?: string; + /** + * The authentication client secret. + */ clientSecret?: string; + /** + * The authentication refresh token. + */ refreshToken?: string; } @@ -36,21 +45,32 @@ export class UserRefreshClient extends OAuth2Client { _refreshToken?: string | null; /** - * User Refresh Token credentials. + * The User Refresh Token client. * - * @param clientId The authentication client ID. - * @param clientSecret The authentication client secret. - * @param refreshToken The authentication refresh token. + * @param optionsOrClientId The User Refresh Token client options. Passing an `clientId` directly is **@DEPRECATED**. + * @param clientSecret **@DEPRECATED**. Provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead. + * @param refreshToken **@DEPRECATED**. Provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead. + * @param eagerRefreshThresholdMillis **@DEPRECATED**. Provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead. + * @param forceRefreshOnFailure **@DEPRECATED**. Provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead. */ - constructor(clientId?: string, clientSecret?: string, refreshToken?: string); - constructor(options: UserRefreshClientOptions); - constructor(clientId?: string, clientSecret?: string, refreshToken?: string); constructor( optionsOrClientId?: string | UserRefreshClientOptions, - clientSecret?: string, - refreshToken?: string, - eagerRefreshThresholdMillis?: number, - forceRefreshOnFailure?: boolean + /** + * @deprecated - provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead + */ + clientSecret?: UserRefreshClientOptions['clientSecret'], + /** + * @deprecated - provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead + */ + refreshToken?: UserRefreshClientOptions['refreshToken'], + /** + * @deprecated - provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead + */ + eagerRefreshThresholdMillis?: UserRefreshClientOptions['eagerRefreshThresholdMillis'], + /** + * @deprecated - provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead + */ + forceRefreshOnFailure?: UserRefreshClientOptions['forceRefreshOnFailure'] ) { const opts = optionsOrClientId && typeof optionsOrClientId === 'object' diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index c1af2b9b..9d55d89b 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -39,7 +39,7 @@ import { mockStsTokenExchange, getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; -import {AuthClientOptions, DEFAULT_UNIVERSE} from '../src/auth/authclient'; +import {DEFAULT_UNIVERSE} from '../src/auth/authclient'; nock.disableNetConnect(); @@ -52,11 +52,8 @@ interface SampleResponse { class TestExternalAccountClient extends BaseExternalAccountClient { private counter = 0; - constructor( - options: BaseExternalAccountClientOptions, - additionalOptions?: Partial - ) { - super(options, additionalOptions); + constructor(options: BaseExternalAccountClientOptions) { + super(options); this.credentialSourceType = 'test'; } @@ -255,10 +252,10 @@ describe('BaseExternalAccountClient', () => { eagerRefreshThresholdMillis: 5000, forceRefreshOnFailure: true, }; - const client = new TestExternalAccountClient( - externalAccountOptions, - refreshOptions - ); + const client = new TestExternalAccountClient({ + ...externalAccountOptions, + ...refreshOptions, + }); assert.strictEqual( client.forceRefreshOnFailure, @@ -1103,8 +1100,9 @@ describe('BaseExternalAccountClient', () => { }, ]); - const client = new TestExternalAccountClient(externalAccountOptions, { - // Override 5min threshold with 10 second threshold. + // Override 5min threshold with 10 second threshold. + const client = new TestExternalAccountClient({ + ...externalAccountOptions, eagerRefreshThresholdMillis: customThresh, }); const actualResponse = await client.getAccessToken(); @@ -1575,13 +1573,12 @@ describe('BaseExternalAccountClient', () => { }) ); - const client = new TestExternalAccountClient( - externalAccountOptionsWithSA, - { - // Override 5min threshold with 10 second threshold. - eagerRefreshThresholdMillis: customThresh, - } - ); + // Override 5min threshold with 10 second threshold. + const client = new TestExternalAccountClient({ + ...externalAccountOptionsWithSA, + eagerRefreshThresholdMillis: customThresh, + }); + const actualResponse = await client.getAccessToken(); // Confirm raw GaxiosResponse appended to response. @@ -2409,7 +2406,8 @@ describe('BaseExternalAccountClient', () => { .reply(200, Object.assign({}, exampleResponse)), ]; - const client = new TestExternalAccountClient(externalAccountOptions, { + const client = new TestExternalAccountClient({ + ...externalAccountOptions, forceRefreshOnFailure: true, }); const actualResponse = await client.request({ @@ -2534,7 +2532,8 @@ describe('BaseExternalAccountClient', () => { .reply(403), ]; - const client = new TestExternalAccountClient(externalAccountOptions, { + const client = new TestExternalAccountClient({ + ...externalAccountOptions, forceRefreshOnFailure: true, }); await assert.rejects( diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index f717cc2b..d4ae765f 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -128,7 +128,10 @@ describe('DownscopedClient', () => { }, }; assert.throws(() => { - return new DownscopedClient(client, cabWithEmptyAccessBoundaryRules); + return new DownscopedClient({ + authClient: client, + credentialAccessBoundary: cabWithEmptyAccessBoundaryRules, + }); }, expectedError); }); @@ -284,12 +287,12 @@ describe('DownscopedClient', () => { }, }; assert.doesNotThrow(() => { - return new DownscopedClient( + const instance = new DownscopedClient( client, - cabWithOneAccessBoundaryRule, - undefined, - quotaProjectId + cabWithOneAccessBoundaryRule ); + instance.quotaProjectId = quotaProjectId; + return instance; }); }); @@ -313,9 +316,12 @@ describe('DownscopedClient', () => { }; const downscopedClient = new DownscopedClient( client, - cabWithOneAccessBoundaryRules, - refreshOptions + cabWithOneAccessBoundaryRules ); + downscopedClient.eagerRefreshThresholdMillis = + refreshOptions.eagerRefreshThresholdMillis; + downscopedClient.forceRefreshOnFailure = + refreshOptions.forceRefreshOnFailure; assert.strictEqual( downscopedClient.forceRefreshOnFailure, refreshOptions.forceRefreshOnFailure @@ -755,12 +761,8 @@ describe('DownscopedClient', () => { }, ]); - const cabClient = new DownscopedClient( - client, - testClientAccessBoundary, - undefined, - quotaProjectId - ); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + cabClient.quotaProjectId = quotaProjectId; const actualHeaders = await cabClient.getRequestHeaders(); assert.deepStrictEqual(expectedHeaders, actualHeaders); @@ -839,12 +841,8 @@ describe('DownscopedClient', () => { .reply(200, Object.assign({}, exampleResponse)), ]; - const cabClient = new DownscopedClient( - client, - testClientAccessBoundary, - undefined, - quotaProjectId - ); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + cabClient.quotaProjectId = quotaProjectId; const actualResponse = await cabClient.request({ url: 'https://example.com/api', method: 'POST', @@ -894,12 +892,8 @@ describe('DownscopedClient', () => { .reply(200, Object.assign({}, exampleResponse)), ]; - const cabClient = new DownscopedClient( - client, - testClientAccessBoundary, - undefined, - quotaProjectId - ); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + cabClient.quotaProjectId = quotaProjectId; // Send request with no headers. const actualResponse = await cabClient.request({ url: 'https://example.com/api', @@ -1123,9 +1117,8 @@ describe('DownscopedClient', () => { .reply(200, Object.assign({}, exampleResponse)), ]; - const cabClient = new DownscopedClient(client, testClientAccessBoundary, { - forceRefreshOnFailure: true, - }); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + cabClient.forceRefreshOnFailure = true; const actualResponse = await cabClient.request({ url: 'https://example.com/api', method: 'POST', @@ -1173,9 +1166,8 @@ describe('DownscopedClient', () => { .reply(401), ]; - const cabClient = new DownscopedClient(client, testClientAccessBoundary, { - forceRefreshOnFailure: false, - }); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + cabClient.forceRefreshOnFailure = false; await assert.rejects( cabClient.request({ url: 'https://example.com/api', @@ -1250,9 +1242,8 @@ describe('DownscopedClient', () => { .reply(403), ]; - const cabClient = new DownscopedClient(client, testClientAccessBoundary, { - forceRefreshOnFailure: true, - }); + const cabClient = new DownscopedClient(client, testClientAccessBoundary); + cabClient.forceRefreshOnFailure = true; await assert.rejects( cabClient.request({ url: 'https://example.com/api', diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 81ec5fd2..8bdb926e 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -182,10 +182,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { eagerRefreshThresholdMillis: 5000, forceRefreshOnFailure: true, }; - const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions, - refreshOptions - ); + const client = new ExternalAccountAuthorizedUserClient({ + ...externalAccountAuthorizedUserCredentialOptions, + ...refreshOptions, + }); assert.strictEqual( client.forceRefreshOnFailure, @@ -707,12 +707,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { .reply(200, Object.assign({}, exampleResponse)), ]; - const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions, - { - forceRefreshOnFailure: true, - } - ); + const client = new ExternalAccountAuthorizedUserClient({ + ...externalAccountAuthorizedUserCredentialOptions, + forceRefreshOnFailure: true, + }); const actualResponse = await client.request({ url: 'https://example.com/api', method: 'POST', @@ -755,12 +753,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { .reply(401), ]; - const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions, - { - forceRefreshOnFailure: false, - } - ); + const client = new ExternalAccountAuthorizedUserClient({ + ...externalAccountAuthorizedUserCredentialOptions, + forceRefreshOnFailure: false, + }); await assert.rejects( client.request({ url: 'https://example.com/api', @@ -813,12 +809,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { .reply(403), ]; - const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions, - { - forceRefreshOnFailure: true, - } - ); + const client = new ExternalAccountAuthorizedUserClient({ + ...externalAccountAuthorizedUserCredentialOptions, + forceRefreshOnFailure: true, + }); await assert.rejects( client.request({ url: 'https://example.com/api', diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index 4a083dff..54a89d53 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -118,13 +118,16 @@ describe('ExternalAccountClient', () => { }); it('should return IdentityPoolClient with expected RefreshOptions', () => { - const expectedClient = new IdentityPoolClient( - fileSourcedOptions, - refreshOptions - ); + const expectedClient = new IdentityPoolClient({ + ...fileSourcedOptions, + ...refreshOptions, + }); assert.deepStrictEqual( - ExternalAccountClient.fromJSON(fileSourcedOptions, refreshOptions), + ExternalAccountClient.fromJSON({ + ...fileSourcedOptions, + ...refreshOptions, + }), expectedClient ); }); @@ -139,10 +142,10 @@ describe('ExternalAccountClient', () => { }); it('should return AwsClient with expected RefreshOptions', () => { - const expectedClient = new AwsClient(awsOptions, refreshOptions); + const expectedClient = new AwsClient({...awsOptions, ...refreshOptions}); assert.deepStrictEqual( - ExternalAccountClient.fromJSON(awsOptions, refreshOptions), + ExternalAccountClient.fromJSON({...awsOptions, ...refreshOptions}), expectedClient ); }); @@ -187,16 +190,16 @@ describe('ExternalAccountClient', () => { }); it('should return PluggableAuthClient with expected RefreshOptions', () => { - const expectedClient = new PluggableAuthClient( - pluggableAuthClientOptions, - refreshOptions - ); + const expectedClient = new PluggableAuthClient({ + ...pluggableAuthClientOptions, + ...refreshOptions, + }); assert.deepStrictEqual( - ExternalAccountClient.fromJSON( - pluggableAuthClientOptions, - refreshOptions - ), + ExternalAccountClient.fromJSON({ + ...pluggableAuthClientOptions, + ...refreshOptions, + }), expectedClient ); }); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index ebea50c0..f05d4844 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -39,7 +39,6 @@ import { ExternalAccountClient, OAuth2Client, ExternalAccountClientOptions, - RefreshOptions, Impersonated, IdentityPoolClient, } from '../src'; @@ -1802,7 +1801,7 @@ describe('googleauth', () => { describe('for external_account types', () => { let fromJsonSpy: sinon.SinonSpy< - [ExternalAccountClientOptions, RefreshOptions?], + [ExternalAccountClientOptions], BaseExternalAccountClient | null >; const stsSuccessfulResponse = { @@ -1922,16 +1921,13 @@ describe('googleauth', () => { * @param actualClient The actual client to assert. * @param json The expected JSON object that the client should be * initialized with. - * @param options The expected RefreshOptions the client should be - * initialized with. */ function assertExternalAccountClientInitialized( actualClient: AuthClient, - json: ExternalAccountClientOptions, - options: RefreshOptions + json: ExternalAccountClientOptions ) { // Confirm expected client is initialized. - assert(fromJsonSpy.calledOnceWithExactly(json, options)); + assert(fromJsonSpy.calledOnceWithExactly(json)); assert(fromJsonSpy.returned(actualClient as BaseExternalAccountClient)); } @@ -1951,7 +1947,7 @@ describe('googleauth', () => { const json = createExternalAccountJSON(); const result = auth.fromJSON(json); - assertExternalAccountClientInitialized(result, json, {}); + assertExternalAccountClientInitialized(result, json); }); it('should honor defaultScopes when no user scopes are available', () => { @@ -1959,7 +1955,7 @@ describe('googleauth', () => { auth.defaultScopes = defaultScopes; const result = auth.fromJSON(json); - assertExternalAccountClientInitialized(result, json, {}); + assertExternalAccountClientInitialized(result, json); assert.strictEqual( (result as BaseExternalAccountClient).scopes, defaultScopes @@ -1972,7 +1968,7 @@ describe('googleauth', () => { auth.defaultScopes = defaultScopes; const result = auth.fromJSON(json); - assertExternalAccountClientInitialized(result, json, {}); + assertExternalAccountClientInitialized(result, json); assert.strictEqual( (result as BaseExternalAccountClient).scopes, userScopes @@ -1983,7 +1979,10 @@ describe('googleauth', () => { const json = createExternalAccountJSON(); const result = auth.fromJSON(json, refreshOptions); - assertExternalAccountClientInitialized(result, json, refreshOptions); + assertExternalAccountClientInitialized(result, { + ...json, + ...refreshOptions, + }); }); it('should throw on invalid json', () => { @@ -2006,8 +2005,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( actualClient, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); }); @@ -2018,11 +2016,10 @@ describe('googleauth', () => { const auth = new GoogleAuth(); const result = await auth.fromStream(stream, refreshOptions); - assertExternalAccountClientInitialized( - result, - createExternalAccountJSON(), - refreshOptions - ); + assertExternalAccountClientInitialized(result, { + ...createExternalAccountJSON(), + ...refreshOptions, + }); }); }); @@ -2041,8 +2038,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); // Project ID should also be set. assert.deepEqual(client.projectId, projectId); @@ -2065,8 +2061,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); assert.strictEqual( (client as BaseExternalAccountClient).scopes, @@ -2091,8 +2086,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); assert.strictEqual( (client as BaseExternalAccountClient).scopes, @@ -2113,8 +2107,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); assert.deepEqual(client.projectId, projectId); scopes.forEach(s => s.done()); @@ -2134,8 +2127,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); assert.strictEqual( (client as BaseExternalAccountClient).scopes, @@ -2158,8 +2150,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); assert.strictEqual( (client as BaseExternalAccountClient).scopes, @@ -2201,8 +2192,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( actualClient, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); }); @@ -2212,11 +2202,10 @@ describe('googleauth', () => { refreshOptions ); - assertExternalAccountClientInitialized( - result, - createExternalAccountJSON(), - refreshOptions - ); + assertExternalAccountClientInitialized(result, { + ...createExternalAccountJSON(), + ...refreshOptions, + }); }); }); @@ -2303,11 +2292,10 @@ describe('googleauth', () => { ); assert(result); - assertExternalAccountClientInitialized( - result as AuthClient, - createExternalAccountJSON(), - refreshOptions - ); + assertExternalAccountClientInitialized(result as AuthClient, { + ...createExternalAccountJSON(), + ...refreshOptions, + }); }); it('tryGetApplicationCredentialsFromWellKnownFile() should resolve', async () => { @@ -2319,11 +2307,10 @@ describe('googleauth', () => { ); assert(result); - assertExternalAccountClientInitialized( - result as AuthClient, - createExternalAccountJSON(), - refreshOptions - ); + assertExternalAccountClientInitialized(result as AuthClient, { + ...createExternalAccountJSON(), + ...refreshOptions, + }); }); it('getApplicationCredentialsFromFilePath() should resolve', async () => { @@ -2332,11 +2319,10 @@ describe('googleauth', () => { refreshOptions ); - assertExternalAccountClientInitialized( - result, - createExternalAccountJSON(), - refreshOptions - ); + assertExternalAccountClientInitialized(result, { + ...createExternalAccountJSON(), + ...refreshOptions, + }); }); describe('getClient()', () => { @@ -2348,8 +2334,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( actualClient, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); }); @@ -2360,8 +2345,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( actualClient, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); }); @@ -2377,8 +2361,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON(), - {} + createExternalAccountJSON() ); scopes.forEach(s => s.done()); }); diff --git a/test/test.impersonated.ts b/test/test.impersonated.ts index 12ca0e65..a0dfcee2 100644 --- a/test/test.impersonated.ts +++ b/test/test.impersonated.ts @@ -33,13 +33,12 @@ function createGTokenMock(body: CredentialRequest) { } function createSampleJWTClient() { - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); return jwt; } @@ -294,11 +293,11 @@ describe('impersonated', () => { }), ]; - const source_client = new UserRefreshClient( - 'CLIENT_ID', - 'CLIENT_SECRET', - 'REFRESH_TOKEN' - ); + const source_client = new UserRefreshClient({ + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + refreshToken: 'REFRESH_TOKEN', + }); const impersonated = new Impersonated({ sourceClient: source_client, targetPrincipal: 'target@project.iam.gserviceaccount.com', diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 1d1ee013..6ed2e3a7 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -80,13 +80,12 @@ describe('jwt', () => { }); it('should get an initial access token', done => { - const jwt = new JWT( - 'foo@serviceaccount.com', - PEM_PATH, - undefined, - ['http://bar', 'http://foo'], - 'bar@subjectaccount.com' - ); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + keyFile: PEM_PATH, + scopes: ['http://bar', 'http://foo'], + subject: 'bar@subjectaccount.com', + }); const scope = createGTokenMock({access_token: 'initial-access-token'}); jwt.authorize((err, creds) => { scope.done(); From 2d8115779426d8e88aa7e1e8b527ef897f296cab Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:25:24 -0800 Subject: [PATCH 584/662] refactor: downscopedClient fix (#1935) For some reason the Kokoro build passed: --- src/auth/downscopedclient.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 5d9bbc70..9b237bc8 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -164,11 +164,12 @@ export class DownscopedClient extends AuthClient { // Check 1-10 Access Boundary Rules are defined within Credential Access // Boundary. if ( - credentialAccessBoundary.accessBoundary.accessBoundaryRules.length === 0 + this.credentialAccessBoundary.accessBoundary.accessBoundaryRules + .length === 0 ) { throw new Error('At least one access boundary rule needs to be defined.'); } else if ( - credentialAccessBoundary.accessBoundary.accessBoundaryRules.length > + this.credentialAccessBoundary.accessBoundary.accessBoundaryRules.length > MAX_ACCESS_BOUNDARY_RULES_COUNT ) { throw new Error( @@ -179,7 +180,7 @@ export class DownscopedClient extends AuthClient { // Check at least one permission should be defined in each Access Boundary // Rule. - for (const rule of credentialAccessBoundary.accessBoundary + for (const rule of this.credentialAccessBoundary.accessBoundary .accessBoundaryRules) { if (rule.availablePermissions.length === 0) { throw new Error( From aea893cd03bebee884692bc1bf21c483f89345f7 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Wed, 5 Feb 2025 09:57:52 -0800 Subject: [PATCH 585/662] fix: Circular Dependencies Issue (#1936) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Shard the crypto files to prevent circular dep warnings * refactor: Move `ExecutableError` to handler to avoid circular dep * refactor: export * from shared crypto * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- README.md | 12 ++--- src/auth/pluggable-auth-client.ts | 22 ++------ src/auth/pluggable-auth-handler.ts | 19 ++++++- src/crypto/browser/crypto.ts | 2 +- src/crypto/crypto.ts | 61 +--------------------- src/crypto/node/crypto.ts | 3 +- src/crypto/shared.ts | 83 ++++++++++++++++++++++++++++++ 7 files changed, 115 insertions(+), 87 deletions(-) create mode 100644 src/crypto/shared.ts diff --git a/README.md b/README.md index 65b5b138..f1f6966f 100644 --- a/README.md +++ b/README.md @@ -147,11 +147,11 @@ function getAuthenticatedClient() { return new Promise((resolve, reject) => { // create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file, // which should be downloaded from the Google Developers Console. - const oAuth2Client = new OAuth2Client( - keys.web.client_id, - keys.web.client_secret, - keys.web.redirect_uris[0] - ); + const oAuth2Client = new OAuth2Client({ + clientId: keys.web.client_id, + clientSecret: keys.web.client_secret, + redirectUri: keys.web.redirect_uris[0] + }); // Generate the url that will be used for the consent dialog. const authorizeUrl = oAuth2Client.generateAuthUrl({ @@ -1304,7 +1304,7 @@ const googleAuth = new GoogleAuth({ const client = await googleAuth.getClient(); // Use the client to create a DownscopedClient. -const cabClient = new DownscopedClient(client, cab); +const cabClient = new DownscopedClient({authClient: client, credentialAccessBoundary: cab}); // Refresh the tokens. const refreshedAccessToken = await cabClient.getAccessToken(); diff --git a/src/auth/pluggable-auth-client.ts b/src/auth/pluggable-auth-client.ts index ad9bbd23..f1539321 100644 --- a/src/auth/pluggable-auth-client.ts +++ b/src/auth/pluggable-auth-client.ts @@ -20,7 +20,9 @@ import { ExecutableResponse, InvalidExpirationTimeFieldError, } from './executable-response'; -import {PluggableAuthHandler} from './pluggable-auth-handler'; +import {PluggableAuthHandler, ExecutableError} from './pluggable-auth-handler'; + +export {ExecutableError} from './pluggable-auth-handler'; /** * Defines the credential source portion of the configuration for PluggableAuthClient. @@ -64,24 +66,6 @@ export interface PluggableAuthClientOptions }; } -/** - * Error thrown from the executable run by PluggableAuthClient. - */ -export class ExecutableError extends Error { - /** - * The exit code returned by the executable. - */ - readonly code: string; - - constructor(message: string, code: string) { - super( - `The executable failed with exit code: ${code} and error message: ${message}.` - ); - this.code = code; - Object.setPrototypeOf(this, new.target.prototype); - } -} - /** * The default executable timeout when none is provided, in milliseconds. */ diff --git a/src/auth/pluggable-auth-handler.ts b/src/auth/pluggable-auth-handler.ts index 29dc2e17..b56af076 100644 --- a/src/auth/pluggable-auth-handler.ts +++ b/src/auth/pluggable-auth-handler.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ExecutableError} from './pluggable-auth-client'; import { ExecutableResponse, ExecutableResponseError, @@ -21,6 +20,24 @@ import { import * as childProcess from 'child_process'; import * as fs from 'fs'; +/** + * Error thrown from the executable run by PluggableAuthClient. + */ +export class ExecutableError extends Error { + /** + * The exit code returned by the executable. + */ + readonly code: string; + + constructor(message: string, code: string) { + super( + `The executable failed with exit code: ${code} and error message: ${message}.` + ); + this.code = code; + Object.setPrototypeOf(this, new.target.prototype); + } +} + /** * Defines the options used for the PluggableAuthHandler class. */ diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index df584ef1..0a263466 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -18,7 +18,7 @@ import * as base64js from 'base64-js'; -import {Crypto, JwkCertificate, fromArrayBufferToHex} from '../crypto'; +import {Crypto, JwkCertificate, fromArrayBufferToHex} from '../shared'; export class BrowserCrypto implements Crypto { constructor() { diff --git a/src/crypto/crypto.ts b/src/crypto/crypto.ts index be50295e..40bb2b08 100644 --- a/src/crypto/crypto.ts +++ b/src/crypto/crypto.ts @@ -15,15 +15,9 @@ import {BrowserCrypto} from './browser/crypto'; import {NodeCrypto} from './node/crypto'; +import {Crypto} from './shared'; -export interface JwkCertificate { - kty: string; - alg: string; - use?: string; - kid: string; - n: string; - e: string; -} +export * from './shared'; export interface CryptoSigner { update(data: string): void; @@ -37,41 +31,6 @@ export interface CryptoSigner { // SubtleCrypto methods return promises, we must make those // methods return promises here as well, even though in Node.js // they are synchronous. -export interface Crypto { - sha256DigestBase64(str: string): Promise; - randomBytesBase64(n: number): string; - verify( - pubkey: string | JwkCertificate, - data: string | Buffer, - signature: string - ): Promise; - sign( - privateKey: string | JwkCertificate, - data: string | Buffer - ): Promise; - decodeBase64StringUtf8(base64: string): string; - encodeBase64StringUtf8(text: string): string; - /** - * Computes the SHA-256 hash of the provided string. - * @param str The plain text string to hash. - * @return A promise that resolves with the SHA-256 hash of the provided - * string in hexadecimal encoding. - */ - sha256DigestHex(str: string): Promise; - - /** - * Computes the HMAC hash of a message using the provided crypto key and the - * SHA-256 algorithm. - * @param key The secret crypto key in utf-8 or ArrayBuffer format. - * @param msg The plain text message. - * @return A promise that resolves with the HMAC-SHA256 hash in ArrayBuffer - * format. - */ - signWithHmacSha256( - key: string | ArrayBuffer, - msg: string - ): Promise; -} export function createCrypto(): Crypto { if (hasBrowserCrypto()) { @@ -87,19 +46,3 @@ export function hasBrowserCrypto() { typeof window.crypto.subtle !== 'undefined' ); } - -/** - * Converts an ArrayBuffer to a hexadecimal string. - * @param arrayBuffer The ArrayBuffer to convert to hexadecimal string. - * @return The hexadecimal encoding of the ArrayBuffer. - */ -export function fromArrayBufferToHex(arrayBuffer: ArrayBuffer): string { - // Convert buffer to byte array. - const byteArray = Array.from(new Uint8Array(arrayBuffer)); - // Convert bytes to hex string. - return byteArray - .map(byte => { - return byte.toString(16).padStart(2, '0'); - }) - .join(''); -} diff --git a/src/crypto/node/crypto.ts b/src/crypto/node/crypto.ts index 7d045f2b..e4113a6c 100644 --- a/src/crypto/node/crypto.ts +++ b/src/crypto/node/crypto.ts @@ -13,7 +13,8 @@ // limitations under the License. import * as crypto from 'crypto'; -import {Crypto} from '../crypto'; + +import {Crypto} from '../shared'; export class NodeCrypto implements Crypto { async sha256DigestBase64(str: string): Promise { diff --git a/src/crypto/shared.ts b/src/crypto/shared.ts new file mode 100644 index 00000000..e95771be --- /dev/null +++ b/src/crypto/shared.ts @@ -0,0 +1,83 @@ +// 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. + +/** + * Crypto interface will provide required crypto functions. + * Use `createCrypto()` factory function to create an instance + * of Crypto. It will either use Node.js `crypto` module, or + * use browser's SubtleCrypto interface. Since most of the + * SubtleCrypto methods return promises, we must make those + * methods return promises here as well, even though in Node.js + * they are synchronous. + */ +export interface Crypto { + sha256DigestBase64(str: string): Promise; + randomBytesBase64(n: number): string; + verify( + pubkey: string | JwkCertificate, + data: string | Buffer, + signature: string + ): Promise; + sign( + privateKey: string | JwkCertificate, + data: string | Buffer + ): Promise; + decodeBase64StringUtf8(base64: string): string; + encodeBase64StringUtf8(text: string): string; + /** + * Computes the SHA-256 hash of the provided string. + * @param str The plain text string to hash. + * @return A promise that resolves with the SHA-256 hash of the provided + * string in hexadecimal encoding. + */ + sha256DigestHex(str: string): Promise; + + /** + * Computes the HMAC hash of a message using the provided crypto key and the + * SHA-256 algorithm. + * @param key The secret crypto key in utf-8 or ArrayBuffer format. + * @param msg The plain text message. + * @return A promise that resolves with the HMAC-SHA256 hash in ArrayBuffer + * format. + */ + signWithHmacSha256( + key: string | ArrayBuffer, + msg: string + ): Promise; +} + +export interface JwkCertificate { + kty: string; + alg: string; + use?: string; + kid: string; + n: string; + e: string; +} + +/** + * Converts an ArrayBuffer to a hexadecimal string. + * @param arrayBuffer The ArrayBuffer to convert to hexadecimal string. + * @return The hexadecimal encoding of the ArrayBuffer. + */ +export function fromArrayBufferToHex(arrayBuffer: ArrayBuffer): string { + // Convert buffer to byte array. + const byteArray = Array.from(new Uint8Array(arrayBuffer)); + // Convert bytes to hex string. + return byteArray + .map(byte => { + return byte.toString(16).padStart(2, '0'); + }) + .join(''); +} From 474453d64a3ba2a1cfba6d1527a2cd6e60bda62d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 5 Feb 2025 19:21:45 +0100 Subject: [PATCH 586/662] fix(deps): update dependency puppeteer to v24 (#1933) --- package.json | 2 +- samples/puppeteer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 12274350..b7db6859 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "ncp": "^2.0.0", "nock": "^13.0.0", "null-loader": "^4.0.0", - "puppeteer": "^21.0.0", + "puppeteer": "^24.0.0", "sinon": "^18.0.0", "ts-loader": "^8.0.0", "typescript": "^5.1.6", diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index bff1f55f..797c1247 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -12,6 +12,6 @@ "license": "Apache-2.0", "dependencies": { "google-auth-library": "^9.0.0", - "puppeteer": "^21.0.0" + "puppeteer": "^24.0.0" } } From 84d91b9d488d373834f5e5076bc7f34d50f0fd9c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 6 Feb 2025 16:06:26 +0100 Subject: [PATCH 587/662] chore(deps): update dependency c8 to v10 (#1931) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7db6859..6f413370 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@types/node": "^22.0.0", "@types/sinon": "^17.0.0", "assert-rejects": "^1.0.0", - "c8": "^8.0.0", + "c8": "^10.0.0", "chai": "^4.2.0", "codecov": "^3.0.2", "gts": "^5.0.0", From dbcc44bf73c494361f331b3423c679cc2d19d51f Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:34:22 -0800 Subject: [PATCH 588/662] refactor!: Remove `Transporter` (#1937) * refactor: remove `Transporter` * chore: Migrate and deprecate `BodyResponseCallback` * refactor!: Remove `Transporter` * refactor: Use Gaxios Interceptors for default Auth headers * refactor: keep old name to reduce refactoring downstream (if used) * test: Add explicit tests for uniform auth headers * style: lint --- samples/test/externalclient.test.js | 21 +-- src/auth/authclient.ts | 93 +++++++---- src/auth/baseexternalclient.ts | 14 +- .../defaultawssecuritycredentialssupplier.ts | 9 +- src/auth/downscopedclient.ts | 8 +- .../externalAccountAuthorizedUserClient.ts | 39 +++-- src/auth/oauth2client.ts | 2 +- src/auth/oauth2common.ts | 54 ++++-- src/auth/stscredentials.ts | 44 +++-- src/index.ts | 1 - src/shared.cts | 22 +++ src/transporters.ts | 127 --------------- test/test.authclient.ts | 107 +++++++++++- test/test.baseexternalclient.ts | 18 +- test/test.downscopedclient.ts | 7 +- ...est.externalaccountauthorizeduserclient.ts | 7 +- test/test.googleauth.ts | 6 +- test/test.index.ts | 7 - test/test.oauth2.ts | 2 +- test/test.transporters.ts | 154 ------------------ 20 files changed, 333 insertions(+), 409 deletions(-) create mode 100644 src/shared.cts delete mode 100644 src/transporters.ts delete mode 100644 test/test.transporters.ts diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js index 6f128529..57c7bc56 100644 --- a/samples/test/externalclient.test.js +++ b/samples/test/externalclient.test.js @@ -72,11 +72,7 @@ const {assert} = require('chai'); const {describe, it, before, afterEach} = require('mocha'); const fs = require('fs'); const {promisify} = require('util'); -const { - GoogleAuth, - DefaultTransporter, - IdentityPoolClient, -} = require('google-auth-library'); +const {GoogleAuth, IdentityPoolClient, gaxios} = require('google-auth-library'); const os = require('os'); const path = require('path'); const http = require('http'); @@ -158,11 +154,16 @@ const assumeRoleWithWebIdentity = async ( // been configured: // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html const oidcToken = await generateGoogleIdToken(auth, aud, clientEmail); - const transporter = new DefaultTransporter(); - const url = - 'https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity' + - '&Version=2011-06-15&DurationSeconds=3600&RoleSessionName=nodejs-test' + - `&RoleArn=${awsRoleArn}&WebIdentityToken=${oidcToken}`; + const transporter = new gaxios.Gaxios(); + + const url = new URL('https://sts.amazonaws.com/'); + url.searchParams.append('Action', 'AssumeRoleWithWebIdentity'); + url.searchParams.append('Version', '2011-06-15'); + url.searchParams.append('DurationSeconds', '3600'); + url.searchParams.append('RoleSessionName', 'nodejs-test'); + url.searchParams.append('RoleArn', awsRoleArn); + url.searchParams.append('WebIdentityToken', oidcToken); + // The response is in XML format but we will parse it as text. const response = await transporter.request({url, responseType: 'text'}); const rawXml = response.data; diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 1eddeb06..cd7096b4 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -15,10 +15,11 @@ import {EventEmitter} from 'events'; import {Gaxios, GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; -import {DefaultTransporter, Transporter} from '../transporters'; import {Credentials} from './credentials'; import {OriginalAndCamel, originalOrCamelOptions} from '../util'; +import {PRODUCT_NAME, USER_AGENT} from '../shared.cjs'; + /** * Base auth configurations (e.g. from JWT or `.json` files) with conventional * camelCased options. @@ -81,13 +82,17 @@ export interface AuthClientOptions credentials?: Credentials; /** - * A `Gaxios` or `Transporter` instance to use for `AuthClient` requests. + * The {@link Gaxios `Gaxios`} instance used for making requests. + * + * @see {@link AuthClientOptions.useAuthRequestParameters} */ - transporter?: Gaxios | Transporter; + transporter?: Gaxios; /** * Provides default options to the transporter, such as {@link GaxiosOptions.agent `agent`} or * {@link GaxiosOptions.retryConfig `retryConfig`}. + * + * This option is ignored if {@link AuthClientOptions.transporter `gaxios`} has been provided */ transporterOptions?: GaxiosOptions; @@ -103,6 +108,19 @@ export interface AuthClientOptions * on the expiry_date. */ forceRefreshOnFailure?: boolean; + + /** + * Enables/disables the adding of the AuthClient's default interceptor. + * + * @see {@link AuthClientOptions.transporter} + * + * @remarks + * + * Disabling is useful for debugging and experimentation. + * + * @default true + */ + useAuthRequestParameters?: boolean; } /** @@ -183,7 +201,10 @@ export abstract class AuthClient * See {@link https://cloud.google.com/docs/quota Working with quotas} */ quotaProjectId?: string; - transporter: Transporter; + /** + * The {@link Gaxios `Gaxios`} instance used for making requests. + */ + transporter: Gaxios; credentials: Credentials = {}; eagerRefreshThresholdMillis = DEFAULT_EAGER_REFRESH_THRESHOLD_MILLIS; forceRefreshOnFailure = false; @@ -202,10 +223,12 @@ export abstract class AuthClient this.universeDomain = options.get('universe_domain') ?? DEFAULT_UNIVERSE; // Shared client options - this.transporter = opts.transporter ?? new DefaultTransporter(); + this.transporter = opts.transporter ?? new Gaxios(opts.transporterOptions); - if (opts.transporterOptions) { - this.transporter.defaults = opts.transporterOptions; + if (options.get('useAuthRequestParameters') !== false) { + this.transporter.interceptors.request.add( + AuthClient.DEFAULT_REQUEST_INTERCEPTOR + ); } if (opts.eagerRefreshThresholdMillis) { @@ -216,29 +239,11 @@ export abstract class AuthClient } /** - * Return the {@link Gaxios `Gaxios`} instance from the {@link AuthClient.transporter}. + * The public request API in which credentials may be added to the request. * - * @expiremental - */ - get gaxios(): Gaxios | null { - if (this.transporter instanceof Gaxios) { - return this.transporter; - } else if (this.transporter instanceof DefaultTransporter) { - return this.transporter.instance; - } else if ( - 'instance' in this.transporter && - this.transporter.instance instanceof Gaxios - ) { - return this.transporter.instance; - } - - return null; - } - - /** - * Provides an alternative Gaxios request implementation with auth credentials + * @param options options for `gaxios` */ - abstract request(opts: GaxiosOptions): GaxiosPromise; + abstract request(options: GaxiosOptions): GaxiosPromise; /** * The main authentication interface. It takes an optional url which when @@ -288,6 +293,31 @@ export abstract class AuthClient return headers; } + static readonly DEFAULT_REQUEST_INTERCEPTOR: Parameters< + Gaxios['interceptors']['request']['add'] + >[0] = { + resolved: async config => { + const headers = config.headers || {}; + + // Set `x-goog-api-client`, if not already set + if (!headers['x-goog-api-client']) { + const nodeVersion = process.version.replace(/^v/, ''); + headers['x-goog-api-client'] = `gl-node/${nodeVersion}`; + } + + // Set `User-Agent` + if (!headers['User-Agent']) { + headers['User-Agent'] = USER_AGENT; + } else if (!headers['User-Agent'].includes(`${PRODUCT_NAME}/`)) { + headers['User-Agent'] = `${headers['User-Agent']} ${USER_AGENT}`; + } + + config.headers = headers; + + return config; + }, + }; + /** * Retry config for Auth-related requests. * @@ -315,3 +345,10 @@ export interface GetAccessTokenResponse { token?: string | null; res?: GaxiosResponse | null; } + +/** + * @deprecated - use the Promise API instead + */ +export interface BodyResponseCallback { + (err: Error | null, res?: GaxiosResponse | null): void; +} diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 5d91a407..2ea85239 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -27,8 +27,8 @@ import { AuthClientOptions, GetAccessTokenResponse, Headers, + BodyResponseCallback, } from './authclient'; -import {BodyResponseCallback, Transporter} from '../transporters'; import * as sts from './stscredentials'; import {ClientAuthentication} from './oauth2common'; import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; @@ -110,10 +110,11 @@ export interface ExternalAccountSupplierContext { * * "urn:ietf:params:oauth:token-type:id_token" */ subjectTokenType: string; - /** The {@link Gaxios} or {@link Transporter} instance from - * the calling external account to use for requests. + /** + * The {@link Gaxios} instance for calling external account + * to use for requests. */ - transporter: Transporter | Gaxios; + transporter: Gaxios; } /** @@ -312,7 +313,10 @@ export abstract class BaseExternalAccountClient extends AuthClient { }; } - this.stsCredential = new sts.StsCredentials(tokenUrl, this.clientAuth); + this.stsCredential = new sts.StsCredentials({ + tokenExchangeEndpoint: tokenUrl, + clientAuthentication: this.clientAuth, + }); this.scopes = opts.get('scopes') || [DEFAULT_OAUTH_SCOPE]; this.cachedAccessToken = null; this.audience = opts.get('audience'); diff --git a/src/auth/defaultawssecuritycredentialssupplier.ts b/src/auth/defaultawssecuritycredentialssupplier.ts index 2c06c134..011064bd 100644 --- a/src/auth/defaultawssecuritycredentialssupplier.ts +++ b/src/auth/defaultawssecuritycredentialssupplier.ts @@ -14,7 +14,6 @@ import {ExternalAccountSupplierContext} from './baseexternalclient'; import {Gaxios, GaxiosOptions} from 'gaxios'; -import {Transporter} from '../transporters'; import {AwsSecurityCredentialsSupplier} from './awsclient'; import {AwsSecurityCredentials} from './awsrequestsigner'; import {Headers} from './authclient'; @@ -183,9 +182,7 @@ export class DefaultAwsSecurityCredentialsSupplier * @param transporter The transporter to use for requests. * @return A promise that resolves with the IMDSv2 Session Token. */ - async #getImdsV2SessionToken( - transporter: Transporter | Gaxios - ): Promise { + async #getImdsV2SessionToken(transporter: Gaxios): Promise { const opts: GaxiosOptions = { ...this.additionalGaxiosOptions, url: this.imdsV2SessionTokenUrl, @@ -205,7 +202,7 @@ export class DefaultAwsSecurityCredentialsSupplier */ async #getAwsRoleName( headers: Headers, - transporter: Transporter | Gaxios + transporter: Gaxios ): Promise { if (!this.securityCredentialsUrl) { throw new Error( @@ -236,7 +233,7 @@ export class DefaultAwsSecurityCredentialsSupplier async #retrieveAwsSecurityCredentials( roleName: string, headers: Headers, - transporter: Transporter | Gaxios + transporter: Gaxios ): Promise { const response = await transporter.request({ ...this.additionalGaxiosOptions, diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 9b237bc8..2a8d7a08 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -20,13 +20,13 @@ import { } from 'gaxios'; import * as stream from 'stream'; -import {BodyResponseCallback} from '../transporters'; import {Credentials} from './credentials'; import { AuthClient, AuthClientOptions, GetAccessTokenResponse, Headers, + BodyResponseCallback, } from './authclient'; import * as sts from './stscredentials'; @@ -189,9 +189,9 @@ export class DownscopedClient extends AuthClient { } } - this.stsCredential = new sts.StsCredentials( - `https://sts.${this.universeDomain}/v1/token` - ); + this.stsCredential = new sts.StsCredentials({ + tokenExchangeEndpoint: `https://sts.${this.universeDomain}/v1/token`, + }); this.cachedDownscopedAccessToken = null; } diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index 3cbef1fd..fb837ceb 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {AuthClient, Headers} from './authclient'; +import {AuthClient, Headers, BodyResponseCallback} from './authclient'; import { ClientAuthentication, getErrorFromOAuthErrorResponse, OAuthClientAuthHandler, + OAuthClientAuthHandlerOptions, OAuthErrorResponse, } from './oauth2common'; -import {BodyResponseCallback, Transporter} from '../transporters'; import { GaxiosError, GaxiosOptions, @@ -69,11 +69,21 @@ interface TokenRefreshResponse { res?: GaxiosResponse | null; } +interface ExternalAccountAuthorizedUserHandlerOptions + extends OAuthClientAuthHandlerOptions { + /** + * The URL of the token refresh endpoint. + */ + tokenRefreshEndpoint: string | URL; +} + /** * Handler for token refresh requests sent to the token_url endpoint for external * authorized user credentials. */ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { + #tokenRefreshEndpoint: string | URL; + /** * Initializes an ExternalAccountAuthorizedUserHandler instance. * @param url The URL of the token refresh endpoint. @@ -81,12 +91,10 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { * @param clientAuthentication The client authentication credentials to use * for the refresh request. */ - constructor( - private readonly url: string, - private readonly transporter: Transporter, - clientAuthentication?: ClientAuthentication - ) { - super(clientAuthentication); + constructor(options: ExternalAccountAuthorizedUserHandlerOptions) { + super(options); + + this.#tokenRefreshEndpoint = options.tokenRefreshEndpoint; } /** @@ -114,7 +122,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { const opts: GaxiosOptions = { ...ExternalAccountAuthorizedUserHandler.RETRY_CONFIG, - url: this.url, + url: this.#tokenRefreshEndpoint, method: 'POST', headers, data: values.toString(), @@ -169,18 +177,19 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { this.universeDomain = options.universe_domain; } this.refreshToken = options.refresh_token; - const clientAuth = { + const clientAuthentication = { confidentialClientType: 'basic', clientId: options.client_id, clientSecret: options.client_secret, } as ClientAuthentication; this.externalAccountAuthorizedUserHandler = - new ExternalAccountAuthorizedUserHandler( - options.token_url ?? + new ExternalAccountAuthorizedUserHandler({ + tokenRefreshEndpoint: + options.token_url ?? DEFAULT_TOKEN_URL.replace('{universeDomain}', this.universeDomain), - this.transporter, - clientAuth - ); + transporter: this.transporter, + clientAuthentication, + }); this.cachedAccessToken = null; this.quotaProjectId = options.quota_project_id; diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index f74720ce..af9efb29 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -23,13 +23,13 @@ import * as stream from 'stream'; import * as formatEcdsa from 'ecdsa-sig-formatter'; import {createCrypto, JwkCertificate, hasBrowserCrypto} from '../crypto/crypto'; -import {BodyResponseCallback} from '../transporters'; import { AuthClient, AuthClientOptions, GetAccessTokenResponse, Headers, + BodyResponseCallback, } from './authclient'; import {CredentialRequest, Credentials} from './credentials'; import {LoginTicket, TokenPayload} from './loginticket'; diff --git a/src/auth/oauth2common.ts b/src/auth/oauth2common.ts index 19f4fe8e..06c37522 100644 --- a/src/auth/oauth2common.ts +++ b/src/auth/oauth2common.ts @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {GaxiosOptions} from 'gaxios'; +import {Gaxios, GaxiosOptions} from 'gaxios'; import * as querystring from 'querystring'; -import {Crypto, createCrypto} from '../crypto/crypto'; +import {createCrypto} from '../crypto/crypto'; /** List of HTTP methods that accept request bodies. */ const METHODS_SUPPORTING_REQUEST_BODY = ['PUT', 'POST', 'PATCH']; @@ -60,6 +60,18 @@ export interface ClientAuthentication { clientSecret?: string; } +export interface OAuthClientAuthHandlerOptions { + /** + * Defines the client authentication credentials for basic and request-body + * credentials. + */ + clientAuthentication?: ClientAuthentication; + /** + * An optional transporter to use. + */ + transporter?: Gaxios; +} + /** * Abstract class for handling client authentication in OAuth-based * operations. @@ -68,14 +80,22 @@ export interface ClientAuthentication { * request bodies are supported. */ export abstract class OAuthClientAuthHandler { - private crypto: Crypto; + #crypto = createCrypto(); + #clientAuthentication?: ClientAuthentication; + protected transporter: Gaxios; /** * Instantiates an OAuth client authentication handler. - * @param clientAuthentication The client auth credentials. + * @param options The OAuth Client Auth Handler instance options. Passing an `ClientAuthentication` directly is **@DEPRECATED**. */ - constructor(private readonly clientAuthentication?: ClientAuthentication) { - this.crypto = createCrypto(); + constructor(options?: ClientAuthentication | OAuthClientAuthHandlerOptions) { + if (options && 'clientId' in options) { + this.#clientAuthentication = options; + this.transporter = new Gaxios(); + } else { + this.#clientAuthentication = options?.clientAuthentication; + this.transporter = options?.transporter || new Gaxios(); + } } /** @@ -117,11 +137,11 @@ export abstract class OAuthClientAuthHandler { Object.assign(opts.headers, { Authorization: `Bearer ${bearerToken}}`, }); - } else if (this.clientAuthentication?.confidentialClientType === 'basic') { + } else if (this.#clientAuthentication?.confidentialClientType === 'basic') { opts.headers = opts.headers || {}; - const clientId = this.clientAuthentication!.clientId; - const clientSecret = this.clientAuthentication!.clientSecret || ''; - const base64EncodedCreds = this.crypto.encodeBase64StringUtf8( + const clientId = this.#clientAuthentication!.clientId; + const clientSecret = this.#clientAuthentication!.clientSecret || ''; + const base64EncodedCreds = this.#crypto.encodeBase64StringUtf8( `${clientId}:${clientSecret}` ); Object.assign(opts.headers, { @@ -138,7 +158,7 @@ export abstract class OAuthClientAuthHandler { * depending on the client authentication mechanism to be used. */ private injectAuthenticatedRequestBody(opts: GaxiosOptions) { - if (this.clientAuthentication?.confidentialClientType === 'request-body') { + if (this.#clientAuthentication?.confidentialClientType === 'request-body') { const method = (opts.method || 'GET').toUpperCase(); // Inject authenticated request body. if (METHODS_SUPPORTING_REQUEST_BODY.indexOf(method) !== -1) { @@ -155,27 +175,27 @@ export abstract class OAuthClientAuthHandler { opts.data = opts.data || ''; const data = querystring.parse(opts.data); Object.assign(data, { - client_id: this.clientAuthentication!.clientId, - client_secret: this.clientAuthentication!.clientSecret || '', + client_id: this.#clientAuthentication!.clientId, + client_secret: this.#clientAuthentication!.clientSecret || '', }); opts.data = querystring.stringify(data); } else if (contentType === 'application/json') { opts.data = opts.data || {}; Object.assign(opts.data, { - client_id: this.clientAuthentication!.clientId, - client_secret: this.clientAuthentication!.clientSecret || '', + client_id: this.#clientAuthentication!.clientId, + client_secret: this.#clientAuthentication!.clientSecret || '', }); } else { throw new Error( `${contentType} content-types are not supported with ` + - `${this.clientAuthentication!.confidentialClientType} ` + + `${this.#clientAuthentication!.confidentialClientType} ` + 'client authentication' ); } } else { throw new Error( `${method} HTTP method does not support ` + - `${this.clientAuthentication!.confidentialClientType} ` + + `${this.#clientAuthentication!.confidentialClientType} ` + 'client authentication' ); } diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index f5a596ac..72bfceb5 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -14,12 +14,11 @@ import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; import * as querystring from 'querystring'; - -import {DefaultTransporter, Transporter} from '../transporters'; import {Headers} from './authclient'; import { ClientAuthentication, OAuthClientAuthHandler, + OAuthClientAuthHandlerOptions, OAuthErrorResponse, getErrorFromOAuthErrorResponse, } from './oauth2common'; @@ -126,25 +125,50 @@ export interface StsSuccessfulResponse { res?: GaxiosResponse | null; } +export interface StsCredentialsConstructionOptions + extends OAuthClientAuthHandlerOptions { + /** + * The client authentication credentials if available. + */ + clientAuthentication?: ClientAuthentication; + /** + * The token exchange endpoint. + */ + tokenExchangeEndpoint: string | URL; +} + /** * Implements the OAuth 2.0 token exchange based on * https://tools.ietf.org/html/rfc8693 */ export class StsCredentials extends OAuthClientAuthHandler { - private transporter: Transporter; + readonly #tokenExchangeEndpoint: string | URL; /** * Initializes an STS credentials instance. - * @param tokenExchangeEndpoint The token exchange endpoint. - * @param clientAuthentication The client authentication credentials if - * available. + * + * @param options The STS credentials instance options. Passing an `tokenExchangeEndpoint` directly is **@DEPRECATED**. + * @param clientAuthentication **@DEPRECATED**. Provide a {@link StsCredentialsConstructionOptions `StsCredentialsConstructionOptions`} object in the first parameter instead. */ constructor( - private readonly tokenExchangeEndpoint: string | URL, + options: StsCredentialsConstructionOptions | string | URL = { + tokenExchangeEndpoint: '', + }, + /** + * @deprecated - provide a {@link StsCredentialsConstructionOptions `StsCredentialsConstructionOptions`} object in the first parameter instead + */ clientAuthentication?: ClientAuthentication ) { - super(clientAuthentication); - this.transporter = new DefaultTransporter(); + if (typeof options !== 'object' || options instanceof URL) { + options = { + tokenExchangeEndpoint: options, + clientAuthentication, + }; + } + + super(options); + + this.#tokenExchangeEndpoint = options.tokenExchangeEndpoint; } /** @@ -196,7 +220,7 @@ export class StsCredentials extends OAuthClientAuthHandler { const opts: GaxiosOptions = { ...StsCredentials.RETRY_CONFIG, - url: this.tokenExchangeEndpoint.toString(), + url: this.#tokenExchangeEndpoint.toString(), method: 'POST', headers, data: querystring.stringify( diff --git a/src/index.ts b/src/index.ts index 6652a7b8..5607d8be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,7 +87,6 @@ export { ExecutableError, } from './auth/pluggable-auth-client'; export {PassThroughClient} from './auth/passthrough'; -export {DefaultTransporter} from './transporters'; type ALL_EXPORTS = (typeof import('./'))[keyof typeof import('./')]; diff --git a/src/shared.cts b/src/shared.cts new file mode 100644 index 00000000..6ecad171 --- /dev/null +++ b/src/shared.cts @@ -0,0 +1,22 @@ +// Copyright 2023 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. + +const pkg: { + name: string; + version: string; +} = require('../../package.json'); + +const PRODUCT_NAME = 'google-api-nodejs-client'; +const USER_AGENT = `${PRODUCT_NAME}/${pkg.version}`; + +export {pkg, PRODUCT_NAME, USER_AGENT}; diff --git a/src/transporters.ts b/src/transporters.ts deleted file mode 100644 index c60a556d..00000000 --- a/src/transporters.ts +++ /dev/null @@ -1,127 +0,0 @@ -// 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. - -import { - Gaxios, - GaxiosError, - GaxiosOptions, - GaxiosPromise, - GaxiosResponse, -} from 'gaxios'; - -const pkg = require('../../package.json'); - -const PRODUCT_NAME = 'google-api-nodejs-client'; - -export interface Transporter { - defaults?: GaxiosOptions; - request(opts: GaxiosOptions): GaxiosPromise; -} - -export interface BodyResponseCallback { - // The `body` object is a truly dynamic type. It must be `any`. - (err: Error | null, res?: GaxiosResponse | null): void; -} - -export interface RequestError extends GaxiosError { - errors: Error[]; -} - -export class DefaultTransporter implements Transporter { - /** - * Default user agent. - */ - static readonly USER_AGENT = `${PRODUCT_NAME}/${pkg.version}`; - - /** - * A configurable, replacable `Gaxios` instance. - */ - instance = new Gaxios(); - - /** - * Configures request options before making a request. - * @param opts GaxiosOptions options. - * @return Configured options. - */ - configure(opts: GaxiosOptions = {}): GaxiosOptions { - opts.headers = opts.headers || {}; - if (typeof window === 'undefined') { - // set transporter user agent if not in browser - const uaValue: string = opts.headers['User-Agent']; - if (!uaValue) { - opts.headers['User-Agent'] = DefaultTransporter.USER_AGENT; - } else if (!uaValue.includes(`${PRODUCT_NAME}/`)) { - opts.headers['User-Agent'] = - `${uaValue} ${DefaultTransporter.USER_AGENT}`; - } - // track google-auth-library-nodejs version: - if (!opts.headers['x-goog-api-client']) { - const nodeVersion = process.version.replace(/^v/, ''); - opts.headers['x-goog-api-client'] = `gl-node/${nodeVersion}`; - } - } - return opts; - } - - /** - * Makes a request using Gaxios with given options. - * @param opts GaxiosOptions options. - * @param callback optional callback that contains GaxiosResponse object. - * @return GaxiosPromise, assuming no callback is passed. - */ - request(opts: GaxiosOptions): GaxiosPromise { - // ensure the user isn't passing in request-style options - opts = this.configure(opts); - return this.instance.request(opts).catch(e => { - throw this.processError(e); - }); - } - - get defaults() { - return this.instance.defaults; - } - - set defaults(opts: GaxiosOptions) { - this.instance.defaults = opts; - } - - /** - * Changes the error to include details from the body. - */ - private processError(e: GaxiosError): RequestError { - const res = e.response; - const err = e as RequestError; - const body = res ? res.data : null; - if (res && body && body.error && res.status !== 200) { - if (typeof body.error === 'string') { - err.message = body.error; - err.status = res.status; - } else if (Array.isArray(body.error.errors)) { - err.message = body.error.errors - .map((err2: Error) => err2.message) - .join('\n'); - err.code = body.error.code; - err.errors = body.error.errors; - } else { - err.message = body.error.message; - err.code = body.error.code; - } - } else if (res && res.status >= 400) { - // Consider all 4xx and 5xx responses errors. - err.message = body; - err.status = res.status; - } - return err; - } -} diff --git a/test/test.authclient.ts b/test/test.authclient.ts index 2faa0f15..2150bb4a 100644 --- a/test/test.authclient.ts +++ b/test/test.authclient.ts @@ -14,8 +14,11 @@ import {strict as assert} from 'assert'; -import {PassThroughClient} from '../src'; +import {Gaxios, GaxiosOptions} from 'gaxios'; + +import {AuthClient, PassThroughClient} from '../src'; import {snakeToCamel} from '../src/util'; +import {PRODUCT_NAME, USER_AGENT} from '../src/shared.cjs'; describe('AuthClient', () => { it('should accept and normalize snake case options to camel case', () => { @@ -38,4 +41,106 @@ describe('AuthClient', () => { assert.equal(authClient[camelCased], value); } }); + + describe('shared auth interceptor', () => { + it('should use the default interceptor', () => { + const gaxios = new Gaxios(); + + new PassThroughClient({transporter: gaxios}); + + assert( + gaxios.interceptors.request.has(AuthClient.DEFAULT_REQUEST_INTERCEPTOR) + ); + }); + + it('should allow disabling of the default interceptor', () => { + const gaxios = new Gaxios(); + const originalInterceptorCount = gaxios.interceptors.request.size; + + const authClient = new PassThroughClient({ + transporter: gaxios, + useAuthRequestParameters: false, + }); + + assert.equal(authClient.transporter, gaxios); + assert.equal( + authClient.transporter.interceptors.request.size, + originalInterceptorCount + ); + }); + + it('should add the default interceptor exactly once between instances', () => { + const gaxios = new Gaxios(); + const originalInterceptorCount = gaxios.interceptors.request.size; + const expectedInterceptorCount = originalInterceptorCount + 1; + + new PassThroughClient({transporter: gaxios}); + new PassThroughClient({transporter: gaxios}); + + assert.equal(gaxios.interceptors.request.size, expectedInterceptorCount); + }); + + describe('User-Agent', () => { + it('should set the header if it does not exist', async () => { + const options: GaxiosOptions = {}; + + await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); + + assert.equal(options.headers?.['User-Agent'], USER_AGENT); + }); + + it('should append to the header if it does exist and does not have the product name', async () => { + const base = 'ABC XYZ'; + const expected = `${base} ${USER_AGENT}`; + const options: GaxiosOptions = { + headers: { + 'User-Agent': base, + }, + }; + + await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); + + assert.equal(options.headers?.['User-Agent'], expected); + }); + + it('should not append to the header if it does exist and does have the product name', async () => { + const expected = `ABC ${PRODUCT_NAME}/XYZ`; + const options: GaxiosOptions = { + headers: { + 'User-Agent': expected, + }, + }; + + await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); + + assert.equal(options.headers?.['User-Agent'], expected); + }); + }); + + describe('x-goog-api-client', () => { + it('should set the header if it does not exist', async () => { + const options: GaxiosOptions = {}; + + await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); + + assert.equal( + options.headers?.['x-goog-api-client'], + `gl-node/${process.version.replace(/^v/, '')}` + ); + }); + + it('should not overwrite an existing header', async () => { + const expected = 'abc'; + const options: GaxiosOptions = { + headers: { + 'x-goog-api-client': expected, + }, + }; + + await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); + + assert.equal(options.headers?.['x-goog-api-client'], expected); + }); + }); + }); }); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 9d55d89b..ad9743d2 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -629,11 +629,7 @@ describe('BaseExternalAccountClient', () => { ]; const client = new TestExternalAccountClient(options); - await assert.rejects( - client.getProjectId(), - /The caller does not have permission/ - ); - + await assert.rejects(client.getProjectId(), GaxiosError); assert.strictEqual(client.projectId, null); scopes.forEach(scope => scope.done()); }); @@ -1326,10 +1322,7 @@ describe('BaseExternalAccountClient', () => { const client = new TestExternalAccountClient( externalAccountOptionsWithSA ); - await assert.rejects( - client.getAccessToken(), - new RegExp(saErrorResponse.error.message) - ); + await assert.rejects(client.getAccessToken(), GaxiosError); // Next try should succeed. const actualResponse = await client.getAccessToken(); // Confirm raw GaxiosResponse appended to response. @@ -2335,9 +2328,10 @@ describe('BaseExternalAccountClient', () => { data: exampleRequest, responseType: 'json', }, - (err, result) => { - assert.strictEqual(err!.message, errorMessage); - assert.deepStrictEqual(result, (err as GaxiosError)!.response); + err => { + assert(err instanceof GaxiosError); + assert.equal(err.status, 400); + scopes.forEach(scope => scope.done()); done(); } diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index d4ae765f..563bb758 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -1046,9 +1046,10 @@ describe('DownscopedClient', () => { data: exampleRequest, responseType: 'json', }, - (err, result) => { - assert.strictEqual(err!.message, errorMessage); - assert.deepStrictEqual(result, (err as GaxiosError)!.response); + err => { + assert(err instanceof GaxiosError); + assert.equal(err.status, 400); + scopes.forEach(scope => scope.done()); done(); } diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 8bdb926e..3f86856d 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -658,9 +658,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { data: exampleRequest, responseType: 'json', }, - (err, result) => { - assert.strictEqual(err!.message, errorMessage); - assert.deepStrictEqual(result, (err as GaxiosError)!.response); + err => { + assert(err instanceof GaxiosError); + assert.equal(err.status, 400); + scopes.forEach(scope => scope.done()); done(); } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index f05d4844..99e6b03c 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -62,6 +62,7 @@ import {stringify} from 'querystring'; import {GoogleAuthExceptionMessages} from '../src/auth/googleauth'; import {IMPERSONATED_ACCOUNT_TYPE} from '../src/auth/impersonated'; import {USER_REFRESH_ACCOUNT_TYPE} from '../src/auth/refreshclient'; +import {GaxiosError} from 'gaxios'; nock.disableNetConnect(); @@ -2250,10 +2251,7 @@ describe('googleauth', () => { const keyFilename = './test/fixtures/external-account-cred.json'; const auth = new GoogleAuth({keyFilename}); - await assert.rejects( - auth.getProjectId(), - /The caller does not have permission/ - ); + await assert.rejects(auth.getProjectId(), GaxiosError); scopes.forEach(s => s.done()); }); diff --git a/test/test.index.ts b/test/test.index.ts index 822eda1c..0c5581d8 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -22,16 +22,9 @@ describe('index', () => { assert.strictEqual(cjs.GoogleAuth, gal.GoogleAuth); }); - it('should publicly export DefaultTransporter', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const cjs = require('../src'); - assert.strictEqual(cjs.DefaultTransporter, gal.DefaultTransporter); - }); - it('should export all the things', () => { assert(gal.CodeChallengeMethod); assert(gal.Compute); - assert(gal.DefaultTransporter); assert(gal.IAMAuth); assert(gal.JWT); assert(gal.JWTAccess); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 7c4d3446..43c27c6d 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1040,7 +1040,7 @@ describe('oauth2', () => { await client.request({url: 'http://example.com'}); } catch (e) { assert(e instanceof GaxiosError); - assert(e.message.includes(JSON.stringify(reAuthErrorBody))); + assert.deepStrictEqual(e.response?.data, reAuthErrorBody); return; } finally { diff --git a/test/test.transporters.ts b/test/test.transporters.ts deleted file mode 100644 index 055ede56..00000000 --- a/test/test.transporters.ts +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2013 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. - -import * as assert from 'assert'; -import {describe, it, afterEach} from 'mocha'; -import {GaxiosOptions} from 'gaxios'; -import * as nock from 'nock'; -import {DefaultTransporter, RequestError} from '../src/transporters'; - -describe('transporters', () => { - const savedEnv = process.env; - afterEach(() => { - process.env = savedEnv; - }); - - nock.disableNetConnect(); - - const defaultUserAgentRE = 'google-api-nodejs-client/\\d+.\\d+.\\d+'; - const transporter = new DefaultTransporter(); - - it('should set default adapter to node.js', () => { - const opts = transporter.configure(); - const re = new RegExp(defaultUserAgentRE); - assert(re.test(opts.headers!['User-Agent'])); - }); - - it('should append default client user agent to the existing user agent', () => { - const applicationName = 'MyTestApplication-1.0'; - const opts = transporter.configure({ - headers: {'User-Agent': applicationName}, - url: '', - }); - const re = new RegExp(applicationName + ' ' + defaultUserAgentRE); - assert(re.test(opts.headers!['User-Agent'])); - }); - - it('should not append default client user agent to the existing user agent more than once', () => { - const appName = 'MyTestApplication-1.0 google-api-nodejs-client/foobear'; - const opts = transporter.configure({ - headers: {'User-Agent': appName}, - url: '', - }); - assert.strictEqual(opts.headers!['User-Agent'], appName); - }); - - it('should add x-goog-api-client header if none exists', () => { - const opts = transporter.configure({ - url: '', - }); - assert(/^gl-node\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client'])); - }); - - it('should append to x-goog-api-client header if it exists', () => { - const opts = transporter.configure({ - headers: {'x-goog-api-client': 'gdcl/1.0.0'}, - url: '', - }); - assert(/^gdcl\/[.-\w$]+$/.test(opts.headers!['x-goog-api-client'])); - }); - - // see: https://github.com/googleapis/google-auth-library-nodejs/issues/819 - it('should not append x-goog-api-client header multiple times', () => { - const opts = { - headers: {'x-goog-api-client': 'gdcl/1.0.0'}, - url: '', - }; - let configuredOpts = transporter.configure(opts); - configuredOpts = transporter.configure(opts); - assert( - /^gdcl\/[.-\w$]+$/.test(configuredOpts.headers!['x-goog-api-client']) - ); - }); - - it('should create a single error from multiple response errors', done => { - const firstError = {message: 'Error 1'}; - const secondError = {message: 'Error 2'}; - const url = 'http://example.com'; - const scope = nock(url) - .get('/') - .reply(400, {error: {code: 500, errors: [firstError, secondError]}}); - transporter.request({url}).then( - () => { - scope.done(); - done('Unexpected promise success'); - }, - error => { - scope.done(); - assert.strictEqual(error!.message, 'Error 1\nError 2'); - assert.strictEqual((error as RequestError).code, 500); - assert.strictEqual((error as RequestError).errors.length, 2); - done(); - } - ); - }); - - it('should return an error for a 404 response', done => { - const url = 'http://example.com'; - const scope = nock(url).get('/').reply(404, 'Not found'); - transporter.request({url}).then( - () => { - scope.done(); - done('Unexpected promise success'); - }, - error => { - scope.done(); - assert.strictEqual(error!.message, 'Not found'); - assert.strictEqual((error as RequestError).status, 404); - done(); - } - ); - }); - - it('should support invocation with async/await', async () => { - const url = 'http://example.com'; - const scope = nock(url).get('/').reply(200); - const res = await transporter.request({url}); - scope.done(); - assert.strictEqual(res.status, 200); - }); - - it('should throw if using async/await', async () => { - const url = 'http://example.com'; - const scope = nock(url).get('/').reply(500, '🦃'); - await assert.rejects(transporter.request({url}), /🦃/); - scope.done(); - }); - - it('should work with a callback', done => { - const url = 'http://example.com'; - const scope = nock(url).get('/').reply(200); - transporter.request({url}).then( - res => { - scope.done(); - assert.strictEqual(res!.status, 200); - done(); - }, - error => { - scope.done(); - done(error); - } - ); - }); -}); From f23e807e27a1d64f774f2bf25e01d263f1ce7db1 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:42:31 -0800 Subject: [PATCH 589/662] feat!: `Request` Revamp (#1938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update `gaxios` * refactor!: Request Revamp * style: lint * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: Node 18-related fixes `Headers` in Node v18 are not case-insensitive * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 4 +- README.md | 4 +- browser-test/test.oauth2.ts | 4 +- package.json | 11 +- samples/idTokenFromMetadataServer.js | 2 +- samples/test/externalclient.test.js | 4 +- src/auth/authclient.ts | 63 +++-- src/auth/awsclient.ts | 16 +- src/auth/awsrequestsigner.ts | 72 +++-- src/auth/baseexternalclient.ts | 31 +-- .../defaultawssecuritycredentialssupplier.ts | 23 +- src/auth/downscopedclient.ts | 22 +- .../externalAccountAuthorizedUserClient.ts | 40 ++- src/auth/googleauth.ts | 25 +- src/auth/idtokenclient.ts | 12 +- src/auth/jwtaccess.ts | 8 +- src/auth/jwtclient.ts | 10 +- src/auth/oauth2client.ts | 71 +++-- src/auth/oauth2common.ts | 79 +++--- src/auth/passthrough.ts | 4 +- src/auth/refreshclient.ts | 7 +- src/auth/stscredentials.ts | 32 +-- src/auth/urlsubjecttokensupplier.ts | 1 - test/externalclienthelper.ts | 11 +- test/test.authclient.ts | 43 +-- test/test.awsclient.ts | 42 +-- test/test.awsrequestsigner.ts | 126 ++++----- test/test.baseexternalclient.ts | 55 ++-- test/test.downscopedclient.ts | 40 ++- ...est.externalaccountauthorizeduserclient.ts | 42 ++- test/test.googleauth.ts | 85 +++--- test/test.identitypoolclient.ts | 4 +- test/test.idtokenclient.ts | 14 +- test/test.impersonated.ts | 8 +- test/test.index.ts | 3 + test/test.jwt.ts | 85 +++--- test/test.jwtaccess.ts | 14 +- test/test.oauth2.ts | 139 +++++----- test/test.oauth2common.ts | 248 ++++++++++-------- test/test.passthroughclient.ts | 4 +- test/test.refresh.ts | 6 +- test/test.stscredentials.ts | 43 +-- 42 files changed, 795 insertions(+), 762 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 23c91acf..480509b6 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -56,7 +56,7 @@ body: |- ## OAuth2 - This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google Authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login). + This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login). In the following examples, you may need a `CLIENT_ID`, `CLIENT_SECRET` and `REDIRECT_URL`. You can find these pieces of information by going to the [Developer Console](https://console.cloud.google.com/), clicking your project > APIs & auth > credentials. @@ -1185,7 +1185,7 @@ body: |- // Get impersonated credentials: const authHeaders = await targetClient.getRequestHeaders(); - // Do something with `authHeaders.Authorization`. + // Do something with `authHeaders.get('authorization')`. // Use impersonated credentials: const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID' diff --git a/README.md b/README.md index f1f6966f..a4594640 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ main().catch(console.error); ## OAuth2 -This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google Authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login). +This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login). In the following examples, you may need a `CLIENT_ID`, `CLIENT_SECRET` and `REDIRECT_URL`. You can find these pieces of information by going to the [Developer Console](https://console.cloud.google.com/), clicking your project > APIs & auth > credentials. @@ -1229,7 +1229,7 @@ async function main() { // Get impersonated credentials: const authHeaders = await targetClient.getRequestHeaders(); - // Do something with `authHeaders.Authorization`. + // Do something with `authHeaders.get('authorization')`. // Use impersonated credentials: const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID' diff --git a/browser-test/test.oauth2.ts b/browser-test/test.oauth2.ts index e7b9cb9a..80e1bbea 100644 --- a/browser-test/test.oauth2.ts +++ b/browser-test/test.oauth2.ts @@ -46,10 +46,10 @@ const FEDERATED_SIGNON_JWK_CERTS = [ }, ]; const FEDERATED_SIGNON_JWK_CERTS_AXIOS_RESPONSE = { - headers: { + headers: new Headers({ 'cache-control': 'cache-control: public, max-age=24000, must-revalidate, no-transform', - }, + }), data: {keys: FEDERATED_SIGNON_JWK_CERTS}, }; diff --git a/package.json b/package.json index 6f413370..cfa97071 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", + "gaxios": "^7.0.0-rc.4", + "gcp-metadata": "^7.0.0-rc.1", + "gtoken": "^8.0.0-rc.1", "jws": "^4.0.0" }, "devDependencies": { @@ -54,7 +54,7 @@ "mocha": "^9.2.2", "mv": "^2.1.1", "ncp": "^2.0.0", - "nock": "^13.0.0", + "nock": "^14.0.1", "null-loader": "^4.0.0", "puppeteer": "^24.0.0", "sinon": "^18.0.0", @@ -84,8 +84,7 @@ "browser-test": "karma start", "docs-test": "linkinator docs", "predocs-test": "npm run docs", - "prelint": "cd samples; npm link ../; npm install", - "precompile": "gts clean" + "prelint": "cd samples; npm link ../; npm install" }, "license": "Apache-2.0" } diff --git a/samples/idTokenFromMetadataServer.js b/samples/idTokenFromMetadataServer.js index e8127136..5cb86b6f 100644 --- a/samples/idTokenFromMetadataServer.js +++ b/samples/idTokenFromMetadataServer.js @@ -14,7 +14,7 @@ /** * Uses the Google Cloud metadata server environment to create an identity token - * and add it to the HTTP request as part of an Authorization header. + * and add it to the HTTP request as part of an authorization header. * * @param {string} targetAudience - The url or target audience to obtain the ID token for. */ diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js index 57c7bc56..67631c62 100644 --- a/samples/test/externalclient.test.js +++ b/samples/test/externalclient.test.js @@ -356,7 +356,7 @@ describe('samples for external-account', () => { if (req.url === '/token' && req.method === 'GET') { // Confirm expected header is passed along the request. if (req.headers['my-header'] === 'some-value') { - res.setHeader('Content-Type', 'application/json'); + res.setHeader('content-type', 'application/json'); res.writeHead(200); res.end( JSON.stringify({ @@ -364,7 +364,7 @@ describe('samples for external-account', () => { }) ); } else { - res.setHeader('Content-Type', 'application/json'); + res.setHeader('content-type', 'application/json'); res.writeHead(400); res.end( JSON.stringify({ diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index cd7096b4..09f09ba4 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -160,10 +160,10 @@ export interface CredentialsClient { * resolves with authorization header fields. * * The result has the form: - * { Authorization: 'Bearer ' } + * { authorization: 'Bearer ' } * @param url The URI being authorized. */ - getRequestHeaders(url?: string): Promise; + getRequestHeaders(url?: string | URL): Promise; /** * Provides an alternative Gaxios request implementation with auth credentials @@ -251,10 +251,13 @@ export abstract class AuthClient * resolves with authorization header fields. * * The result has the form: - * { Authorization: 'Bearer ' } + * ```ts + * new Headers({'authorization': 'Bearer '}); + * ``` + * * @param url The URI being authorized. */ - abstract getRequestHeaders(url?: string): Promise; + abstract getRequestHeaders(url?: string | URL): Promise; /** * @return A promise that resolves with the current GCP access token @@ -285,35 +288,58 @@ export abstract class AuthClient // the x-goog-user-project header, to indicate an alternate account for // billing and quota: if ( - !headers['x-goog-user-project'] && // don't override a value the user sets. + !headers.has('x-goog-user-project') && // don't override a value the user sets. this.quotaProjectId ) { - headers['x-goog-user-project'] = this.quotaProjectId; + headers.set('x-goog-user-project', this.quotaProjectId); } return headers; } + /** + * Adds the `x-goog-user-project` and `authorization` headers to the target Headers + * object, if they exist on the source. + * + * @param target the headers to target + * @param source the headers to source from + * @returns the target headers + */ + protected addUserProjectAndAuthHeaders( + target: T, + source: Headers + ): T { + const xGoogUserProject = source.get('x-goog-user-project'); + const authorizationHeader = source.get('authorization'); + + if (xGoogUserProject) { + target.set('x-goog-user-project', xGoogUserProject); + } + + if (authorizationHeader) { + target.set('authorization', authorizationHeader); + } + + return target; + } + static readonly DEFAULT_REQUEST_INTERCEPTOR: Parameters< Gaxios['interceptors']['request']['add'] >[0] = { resolved: async config => { - const headers = config.headers || {}; - // Set `x-goog-api-client`, if not already set - if (!headers['x-goog-api-client']) { + if (!config.headers.has('x-goog-api-client')) { const nodeVersion = process.version.replace(/^v/, ''); - headers['x-goog-api-client'] = `gl-node/${nodeVersion}`; + config.headers.set('x-goog-api-client', `gl-node/${nodeVersion}`); } // Set `User-Agent` - if (!headers['User-Agent']) { - headers['User-Agent'] = USER_AGENT; - } else if (!headers['User-Agent'].includes(`${PRODUCT_NAME}/`)) { - headers['User-Agent'] = `${headers['User-Agent']} ${USER_AGENT}`; + const userAgent = config.headers.get('User-Agent'); + if (!userAgent) { + config.headers.set('User-Agent', USER_AGENT); + } else if (!userAgent.includes(`${PRODUCT_NAME}/`)) { + config.headers.set('User-Agent', `${userAgent} ${USER_AGENT}`); } - config.headers = headers; - return config; }, }; @@ -337,9 +363,8 @@ export abstract class AuthClient } } -export interface Headers { - [index: string]: string; -} +// TypeScript does not have `HeadersInit` in the standard types yet +export type HeadersInit = ConstructorParameters[0]; export interface GetAccessTokenResponse { token?: string | null; diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index cb296143..504511c6 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -21,6 +21,7 @@ import { import {DefaultAwsSecurityCredentialsSupplier} from './defaultawssecuritycredentialssupplier'; import {originalOrCamelOptions, SnakeToCamelObject} from '../util'; +import {Gaxios} from 'gaxios'; /** * AWS credentials JSON interface. This is used for AWS workloads. @@ -230,7 +231,7 @@ export class AwsClient extends BaseExternalAccountClient { // The GCP STS endpoint expects the headers to be formatted as: // [ // {key: 'x-amz-date', value: '...'}, - // {key: 'Authorization', value: '...'}, + // {key: 'authorization', value: '...'}, // ... // ] // And then serialized as: @@ -240,7 +241,7 @@ export class AwsClient extends BaseExternalAccountClient { // headers: [{key: 'x-amz-date', value: '...'}, ...] // })) const reformattedHeader: {key: string; value: string}[] = []; - const extendedHeaders = Object.assign( + const extendedHeaders = Gaxios.mergeHeaders( { // The full, canonical resource name of the workload identity pool // provider, with or without the HTTPS prefix. @@ -250,13 +251,12 @@ export class AwsClient extends BaseExternalAccountClient { }, options.headers ); + // Reformat header to GCP STS expected format. - for (const key in extendedHeaders) { - reformattedHeader.push({ - key, - value: extendedHeaders[key], - }); - } + extendedHeaders.forEach((value, key) => + reformattedHeader.push({key, value}) + ); + // Serialize the reformatted signed request. return encodeURIComponent( JSON.stringify({ diff --git a/src/auth/awsrequestsigner.ts b/src/auth/awsrequestsigner.ts index cb0126c3..8b95d6e3 100644 --- a/src/auth/awsrequestsigner.ts +++ b/src/auth/awsrequestsigner.ts @@ -12,22 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {GaxiosOptions} from 'gaxios'; +import {Gaxios, GaxiosOptions} from 'gaxios'; -import {Headers} from './authclient'; +import {HeadersInit} from './authclient'; import {Crypto, createCrypto, fromArrayBufferToHex} from '../crypto/crypto'; -type HttpMethod = - | 'GET' - | 'POST' - | 'PUT' - | 'PATCH' - | 'HEAD' - | 'DELETE' - | 'CONNECT' - | 'OPTIONS' - | 'TRACE'; - /** Interface defining the AWS authorization header map for signed requests. */ interface AwsAuthHeaderMap { amzDate?: string; @@ -60,7 +49,7 @@ interface GenerateAuthHeaderMapOptions { // The AWS service URL query string. canonicalQuerystring: string; // The HTTP method used to call this API. - method: HttpMethod; + method: string; // The AWS region. region: string; // The AWS security credentials. @@ -68,7 +57,7 @@ interface GenerateAuthHeaderMapOptions { // The optional request payload if available. requestPayload?: string; // The optional additional headers needed for the requested AWS API. - additionalAmzHeaders?: Headers; + additionalAmzHeaders?: HeadersInit; } /** AWS Signature Version 4 signing algorithm identifier. */ @@ -113,7 +102,7 @@ export class AwsRequestSigner { */ async getRequestOptions(amzOptions: GaxiosOptions): Promise { if (!amzOptions.url) { - throw new Error('"url" is required in "amzOptions"'); + throw new RangeError('"url" is required in "amzOptions"'); } // Stringify JSON requests. This will be set in the request body of the // generated signed request. @@ -127,11 +116,18 @@ export class AwsRequestSigner { const additionalAmzHeaders = amzOptions.headers; const awsSecurityCredentials = await this.getCredentials(); const uri = new URL(url); + + if (typeof requestPayload !== 'string' && requestPayload !== undefined) { + throw new TypeError( + `'requestPayload' is expected to be a string if provided. Got: ${requestPayload}` + ); + } + const headerMap = await generateAuthenticationHeaderMap({ crypto: this.crypto, host: uri.host, canonicalUri: uri.pathname, - canonicalQuerystring: uri.search.substr(1), + canonicalQuerystring: uri.search.slice(1), method, region: this.region, securityCredentials: awsSecurityCredentials, @@ -139,17 +135,17 @@ export class AwsRequestSigner { additionalAmzHeaders, }); // Append additional optional headers, eg. X-Amz-Target, Content-Type, etc. - const headers: {[key: string]: string} = Object.assign( + const headers = Gaxios.mergeHeaders( // Add x-amz-date if available. headerMap.amzDate ? {'x-amz-date': headerMap.amzDate} : {}, { - Authorization: headerMap.authorizationHeader, + authorization: headerMap.authorizationHeader, host: uri.host, }, additionalAmzHeaders || {} ); if (awsSecurityCredentials.token) { - Object.assign(headers, { + Gaxios.mergeHeaders(headers, { 'x-amz-security-token': awsSecurityCredentials.token, }); } @@ -159,7 +155,7 @@ export class AwsRequestSigner { headers, }; - if (typeof requestPayload !== 'undefined') { + if (requestPayload !== undefined) { awsSignedReq.body = requestPayload; } @@ -223,7 +219,9 @@ async function getSigningKey( async function generateAuthenticationHeaderMap( options: GenerateAuthHeaderMapOptions ): Promise { - const additionalAmzHeaders = options.additionalAmzHeaders || {}; + const additionalAmzHeaders = Gaxios.mergeHeaders( + options.additionalAmzHeaders + ); const requestPayload = options.requestPayload || ''; // iam.amazonaws.com host => iam service. // sts.us-east-2.amazonaws.com => sts service. @@ -237,38 +235,38 @@ async function generateAuthenticationHeaderMap( // Format: '%Y%m%d'. const dateStamp = now.toISOString().replace(/[-]/g, '').replace(/T.*/, ''); - // Change all additional headers to be lower case. - const reformattedAdditionalAmzHeaders: Headers = {}; - Object.keys(additionalAmzHeaders).forEach(key => { - reformattedAdditionalAmzHeaders[key.toLowerCase()] = - additionalAmzHeaders[key]; - }); // Add AWS token if available. if (options.securityCredentials.token) { - reformattedAdditionalAmzHeaders['x-amz-security-token'] = - options.securityCredentials.token; + additionalAmzHeaders.set( + 'x-amz-security-token', + options.securityCredentials.token + ); } // Header keys need to be sorted alphabetically. - const amzHeaders = Object.assign( + const amzHeaders = Gaxios.mergeHeaders( { host: options.host, }, // Previously the date was not fixed with x-amz- and could be provided manually. // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req - reformattedAdditionalAmzHeaders.date ? {} : {'x-amz-date': amzDate}, - reformattedAdditionalAmzHeaders + additionalAmzHeaders.has('date') ? {} : {'x-amz-date': amzDate}, + additionalAmzHeaders ); let canonicalHeaders = ''; - const signedHeadersList = Object.keys(amzHeaders).sort(); + + // TypeScript is missing `Headers#keys` at the time of writing + const signedHeadersList = [ + ...(amzHeaders as Headers & {keys: () => string[]}).keys(), + ].sort(); signedHeadersList.forEach(key => { - canonicalHeaders += `${key}:${amzHeaders[key]}\n`; + canonicalHeaders += `${key}:${amzHeaders.get(key)}\n`; }); const signedHeaders = signedHeadersList.join(';'); const payloadHash = await options.crypto.sha256DigestHex(requestPayload); // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html const canonicalRequest = - `${options.method}\n` + + `${options.method.toUpperCase()}\n` + `${options.canonicalUri}\n` + `${options.canonicalQuerystring}\n` + `${canonicalHeaders}\n` + @@ -298,7 +296,7 @@ async function generateAuthenticationHeaderMap( return { // Do not return x-amz-date if date is available. - amzDate: reformattedAdditionalAmzHeaders.date ? undefined : amzDate, + amzDate: additionalAmzHeaders.has('date') ? undefined : amzDate, authorizationHeader, canonicalQuerystring: options.canonicalQuerystring, }; diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 2ea85239..9a39fdf0 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -26,7 +26,6 @@ import { AuthClient, AuthClientOptions, GetAccessTokenResponse, - Headers, BodyResponseCallback, } from './authclient'; import * as sts from './stscredentials'; @@ -416,13 +415,13 @@ export abstract class BaseExternalAccountClient extends AuthClient { * resolves with authorization header fields. * * The result has the form: - * { Authorization: 'Bearer ' } + * { authorization: 'Bearer ' } */ async getRequestHeaders(): Promise { const accessTokenResponse = await this.getAccessToken(); - const headers: Headers = { - Authorization: `Bearer ${accessTokenResponse.token}`, - }; + const headers = new Headers({ + authorization: `Bearer ${accessTokenResponse.token}`, + }); return this.addSharedMetadataHeaders(headers); } @@ -480,7 +479,6 @@ export abstract class BaseExternalAccountClient extends AuthClient { ...BaseExternalAccountClient.RETRY_CONFIG, headers, url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`, - responseType: 'json', }); this.projectId = response.data.projectId; return this.projectId; @@ -502,14 +500,10 @@ export abstract class BaseExternalAccountClient extends AuthClient { let response: GaxiosResponse; try { const requestHeaders = await this.getRequestHeaders(); - opts.headers = opts.headers || {}; - if (requestHeaders && requestHeaders['x-goog-user-project']) { - opts.headers['x-goog-user-project'] = - requestHeaders['x-goog-user-project']; - } - if (requestHeaders && requestHeaders.Authorization) { - opts.headers.Authorization = requestHeaders.Authorization; - } + opts.headers = Gaxios.mergeHeaders(opts.headers); + + this.addUserProjectAndAuthHeaders(opts.headers, requestHeaders); + response = await this.transporter.request(opts); } catch (e) { const res = (e as GaxiosError).response; @@ -588,9 +582,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { !this.clientAuth && this.workforcePoolUserProject ? {userProject: this.workforcePoolUserProject} : undefined; - const additionalHeaders: Headers = { + const additionalHeaders = new Headers({ 'x-goog-api-client': this.getMetricsHeaderValue(), - }; + }); const stsResponse = await this.stsCredential.exchangeToken( stsCredentialsOptions, additionalHeaders, @@ -668,14 +662,13 @@ export abstract class BaseExternalAccountClient extends AuthClient { url: this.serviceAccountImpersonationUrl!, method: 'POST', headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, + 'content-type': 'application/json', + authorization: `Bearer ${token}`, }, data: { scope: this.getScopesArray(), lifetime: this.serviceAccountImpersonationLifetime + 's', }, - responseType: 'json', }; const response = await this.transporter.request(opts); diff --git a/src/auth/defaultawssecuritycredentialssupplier.ts b/src/auth/defaultawssecuritycredentialssupplier.ts index 011064bd..e7a4025e 100644 --- a/src/auth/defaultawssecuritycredentialssupplier.ts +++ b/src/auth/defaultawssecuritycredentialssupplier.ts @@ -16,7 +16,6 @@ import {ExternalAccountSupplierContext} from './baseexternalclient'; import {Gaxios, GaxiosOptions} from 'gaxios'; import {AwsSecurityCredentialsSupplier} from './awsclient'; import {AwsSecurityCredentials} from './awsrequestsigner'; -import {Headers} from './authclient'; /** * Interface defining the AWS security-credentials endpoint response. @@ -110,13 +109,15 @@ export class DefaultAwsSecurityCredentialsSupplier return this.#regionFromEnv; } - const metadataHeaders: Headers = {}; + const metadataHeaders = new Headers(); if (!this.#regionFromEnv && this.imdsV2SessionTokenUrl) { - metadataHeaders['x-aws-ec2-metadata-token'] = - await this.#getImdsV2SessionToken(context.transporter); + metadataHeaders.set( + 'x-aws-ec2-metadata-token', + await this.#getImdsV2SessionToken(context.transporter) + ); } if (!this.regionUrl) { - throw new Error( + throw new RangeError( 'Unable to determine AWS region due to missing ' + '"options.credential_source.region_url"' ); @@ -125,7 +126,6 @@ export class DefaultAwsSecurityCredentialsSupplier ...this.additionalGaxiosOptions, url: this.regionUrl, method: 'GET', - responseType: 'text', headers: metadataHeaders, }; const response = await context.transporter.request(opts); @@ -152,10 +152,12 @@ export class DefaultAwsSecurityCredentialsSupplier return this.#securityCredentialsFromEnv; } - const metadataHeaders: Headers = {}; + const metadataHeaders = new Headers(); if (this.imdsV2SessionTokenUrl) { - metadataHeaders['x-aws-ec2-metadata-token'] = - await this.#getImdsV2SessionToken(context.transporter); + metadataHeaders.set( + 'x-aws-ec2-metadata-token', + await this.#getImdsV2SessionToken(context.transporter) + ); } // Since the role on a VM can change, we don't need to cache it. const roleName = await this.#getAwsRoleName( @@ -187,7 +189,6 @@ export class DefaultAwsSecurityCredentialsSupplier ...this.additionalGaxiosOptions, url: this.imdsV2SessionTokenUrl, method: 'PUT', - responseType: 'text', headers: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }; const response = await transporter.request(opts); @@ -214,7 +215,6 @@ export class DefaultAwsSecurityCredentialsSupplier ...this.additionalGaxiosOptions, url: this.securityCredentialsUrl, method: 'GET', - responseType: 'text', headers: headers, }; const response = await transporter.request(opts); @@ -238,7 +238,6 @@ export class DefaultAwsSecurityCredentialsSupplier const response = await transporter.request({ ...this.additionalGaxiosOptions, url: `${this.securityCredentialsUrl}/${roleName}`, - responseType: 'json', headers: headers, }); return response.data; diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 2a8d7a08..bcc207b0 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -13,6 +13,7 @@ // limitations under the License. import { + Gaxios, GaxiosError, GaxiosOptions, GaxiosPromise, @@ -25,7 +26,6 @@ import { AuthClient, AuthClientOptions, GetAccessTokenResponse, - Headers, BodyResponseCallback, } from './authclient'; @@ -237,13 +237,13 @@ export class DownscopedClient extends AuthClient { * resolves with authorization header fields. * * The result has the form: - * { Authorization: 'Bearer ' } + * { authorization: 'Bearer ' } */ async getRequestHeaders(): Promise { const accessTokenResponse = await this.getAccessToken(); - const headers: Headers = { - Authorization: `Bearer ${accessTokenResponse.token}`, - }; + const headers = new Headers({ + authorization: `Bearer ${accessTokenResponse.token}`, + }); return this.addSharedMetadataHeaders(headers); } @@ -288,14 +288,10 @@ export class DownscopedClient extends AuthClient { let response: GaxiosResponse; try { const requestHeaders = await this.getRequestHeaders(); - opts.headers = opts.headers || {}; - if (requestHeaders && requestHeaders['x-goog-user-project']) { - opts.headers['x-goog-user-project'] = - requestHeaders['x-goog-user-project']; - } - if (requestHeaders && requestHeaders.Authorization) { - opts.headers.Authorization = requestHeaders.Authorization; - } + opts.headers = Gaxios.mergeHeaders(opts.headers); + + this.addUserProjectAndAuthHeaders(opts.headers, requestHeaders); + response = await this.transporter.request(opts); } catch (e) { const res = (e as GaxiosError).response; diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index fb837ceb..ebc43923 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {AuthClient, Headers, BodyResponseCallback} from './authclient'; +import {AuthClient, BodyResponseCallback} from './authclient'; import { ClientAuthentication, getErrorFromOAuthErrorResponse, @@ -21,6 +21,7 @@ import { OAuthErrorResponse, } from './oauth2common'; import { + Gaxios, GaxiosError, GaxiosOptions, GaxiosPromise, @@ -108,26 +109,19 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { */ async refreshToken( refreshToken: string, - additionalHeaders?: Headers + headers?: HeadersInit ): Promise { - const values = new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refreshToken, - }); - - const headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - ...additionalHeaders, - }; - const opts: GaxiosOptions = { ...ExternalAccountAuthorizedUserHandler.RETRY_CONFIG, url: this.#tokenRefreshEndpoint, method: 'POST', headers, - data: values.toString(), - responseType: 'json', + data: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }), }; + // Apply OAuth client authentication. this.applyClientAuthenticationOptions(opts); @@ -224,9 +218,9 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { async getRequestHeaders(): Promise { const accessTokenResponse = await this.getAccessToken(); - const headers: Headers = { - Authorization: `Bearer ${accessTokenResponse.token}`, - }; + const headers = new Headers({ + authorization: `Bearer ${accessTokenResponse.token}`, + }); return this.addSharedMetadataHeaders(headers); } @@ -262,14 +256,10 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { let response: GaxiosResponse; try { const requestHeaders = await this.getRequestHeaders(); - opts.headers = opts.headers || {}; - if (requestHeaders && requestHeaders['x-goog-user-project']) { - opts.headers['x-goog-user-project'] = - requestHeaders['x-goog-user-project']; - } - if (requestHeaders && requestHeaders.Authorization) { - opts.headers.Authorization = requestHeaders.Authorization; - } + opts.headers = Gaxios.mergeHeaders(opts.headers); + + this.addUserProjectAndAuthHeaders(opts.headers, requestHeaders); + response = await this.transporter.request(opts); } catch (e) { const res = (e as GaxiosError).response; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 54c0c312..b55b9f39 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -14,7 +14,7 @@ import {exec} from 'child_process'; import * as fs from 'fs'; -import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; +import {Gaxios, GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; import * as gcpMetadata from 'gcp-metadata'; import * as os from 'os'; import * as path from 'path'; @@ -45,12 +45,7 @@ import { EXTERNAL_ACCOUNT_TYPE, BaseExternalAccountClient, } from './baseexternalclient'; -import { - AuthClient, - AuthClientOptions, - DEFAULT_UNIVERSE, - Headers, -} from './authclient'; +import {AuthClient, AuthClientOptions, DEFAULT_UNIVERSE} from './authclient'; import { EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, ExternalAccountAuthorizedUserClient, @@ -1068,7 +1063,7 @@ export class GoogleAuth { * Obtain the HTTP headers that will provide authorization for a given * request. */ - async getRequestHeaders(url?: string) { + async getRequestHeaders(url?: string | URL) { const client = await this.getClient(); return client.getRequestHeaders(url); } @@ -1078,16 +1073,11 @@ export class GoogleAuth { * the request options. * @param opts Axios or Request options on which to attach the headers */ - async authorizeRequest(opts: { - url?: string; - uri?: string; - headers?: Headers; - }) { - opts = opts || {}; - const url = opts.url || opts.uri; + async authorizeRequest(opts: Pick = {}) { + const url = opts.url; const client = await this.getClient(); const headers = await client.getRequestHeaders(url); - opts.headers = Object.assign(opts.headers || {}, headers); + opts.headers = Gaxios.mergeHeaders(opts.headers, headers); return opts; } @@ -1096,8 +1086,7 @@ export class GoogleAuth { * 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: GaxiosOptions): Promise> { + async request(opts: GaxiosOptions): Promise> { const client = await this.getClient(); return client.request(opts); } diff --git a/src/auth/idtokenclient.ts b/src/auth/idtokenclient.ts index 24d1f859..67d8aca8 100644 --- a/src/auth/idtokenclient.ts +++ b/src/auth/idtokenclient.ts @@ -13,7 +13,6 @@ // limitations under the License. import {Credentials} from './credentials'; -import {Headers} from './authclient'; import { OAuth2Client, OAuth2ClientOptions, @@ -51,10 +50,7 @@ export class IdTokenClient extends OAuth2Client { this.idTokenProvider = options.idTokenProvider; } - protected async getRequestMetadataAsync( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - url?: string | null - ): Promise { + protected async getRequestMetadataAsync(): Promise { if ( !this.credentials.id_token || !this.credentials.expiry_date || @@ -69,9 +65,9 @@ export class IdTokenClient extends OAuth2Client { } as Credentials; } - const headers: Headers = { - Authorization: 'Bearer ' + this.credentials.id_token, - }; + const headers = new Headers({ + authorization: 'Bearer ' + this.credentials.id_token, + }); return {headers}; } diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index 0cabbcbb..2872efbe 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -16,7 +16,6 @@ import * as jws from 'jws'; import * as stream from 'stream'; import {JWTInput} from './credentials'; -import {Headers} from './authclient'; import {LRUCache} from '../util'; const DEFAULT_HEADER: jws.Header = { @@ -107,7 +106,10 @@ export class JWTAccess { cachedToken && cachedToken.expiration - now > this.eagerRefreshThresholdMillis ) { - return cachedToken.headers; + // Copying headers into a new `Headers` object to avoid potential leakage - + // as this is a cache it is possible for multiple requests to reference this + // same value. + return new Headers(cachedToken.headers); } const iat = Math.floor(Date.now() / 1000); @@ -157,7 +159,7 @@ export class JWTAccess { // Sign the jwt and add it to the cache const signedJWT = jws.sign({header, payload, secret: this.key}); - const headers = {Authorization: `Bearer ${signedJWT}`}; + const headers = new Headers({authorization: `Bearer ${signedJWT}`}); this.cache.set(key, { expiration: exp * 1000, headers, diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 8ba1fd0c..c6c308f5 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -141,9 +141,11 @@ export class JWT extends OAuth2Client implements IdTokenProvider { ) { const {tokens} = await this.refreshToken(); return { - headers: this.addSharedMetadataHeaders({ - Authorization: `Bearer ${tokens.id_token}`, - }), + headers: this.addSharedMetadataHeaders( + new Headers({ + authorization: `Bearer ${tokens.id_token}`, + }) + ), }; } else { // no scopes have been set, but a uri has been provided. Use JWTAccess @@ -184,7 +186,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { } else { // If no audience, apiKey, or scopes are provided, we should not attempt // to populate any headers: - return {headers: {}}; + return {headers: new Headers()}; } } diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index af9efb29..e9f903f1 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -13,6 +13,7 @@ // limitations under the License. import { + Gaxios, GaxiosError, GaxiosOptions, GaxiosPromise, @@ -28,7 +29,6 @@ import { AuthClient, AuthClientOptions, GetAccessTokenResponse, - Headers, BodyResponseCallback, } from './authclient'; import {CredentialRequest, Credentials} from './credentials'; @@ -733,9 +733,7 @@ export class OAuth2Client extends AuthClient { options: GetTokenOptions ): Promise { const url = this.endpoints.oauth2TokenUrl.toString(); - const headers: Headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - }; + const headers = new Headers(); const values: GetTokenQuery = { client_id: options.client_id || this._clientId, code_verifier: options.codeVerifier, @@ -746,7 +744,7 @@ export class OAuth2Client extends AuthClient { if (this.clientAuthentication === ClientAuthentication.ClientSecretBasic) { const basic = Buffer.from(`${this._clientId}:${this._clientSecret}`); - headers['Authorization'] = `Basic ${basic.toString('base64')}`; + headers.set('authorization', `Basic ${basic.toString('base64')}`); } if (this.clientAuthentication === ClientAuthentication.ClientSecretPost) { values.client_secret = this._clientSecret; @@ -755,7 +753,7 @@ export class OAuth2Client extends AuthClient { ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, - data: querystring.stringify(values), + data: new URLSearchParams(values as {}), headers, }); const tokens = res.data as Credentials; @@ -805,7 +803,7 @@ export class OAuth2Client extends AuthClient { throw new Error('No refresh token is set.'); } const url = this.endpoints.oauth2TokenUrl.toString(); - const data = { + const data: {} = { refresh_token: refreshToken, client_id: this._clientId, client_secret: this._clientSecret, @@ -820,8 +818,7 @@ export class OAuth2Client extends AuthClient { ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, - data: querystring.stringify(data), - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + data: new URLSearchParams(data), }); } catch (e) { if ( @@ -929,10 +926,10 @@ export class OAuth2Client extends AuthClient { * resolves with authorization header fields. * * In OAuth2Client, the result has the form: - * { Authorization: 'Bearer ' } + * { authorization: 'Bearer ' } * @param url The optional url being authorized */ - async getRequestHeaders(url?: string): Promise { + async getRequestHeaders(url?: string | URL): Promise { const headers = (await this.getRequestMetadataAsync(url)).headers; return headers; } @@ -955,9 +952,9 @@ export class OAuth2Client extends AuthClient { if (thisCreds.access_token && !this.isTokenExpiring()) { thisCreds.token_type = thisCreds.token_type || 'Bearer'; - const headers = { - Authorization: thisCreds.token_type + ' ' + thisCreds.access_token, - }; + const headers = new Headers({ + authorization: thisCreds.token_type + ' ' + thisCreds.access_token, + }); return {headers: this.addSharedMetadataHeaders(headers)}; } @@ -967,15 +964,15 @@ export class OAuth2Client extends AuthClient { await this.processAndValidateRefreshHandler(); if (refreshedAccessToken?.access_token) { this.setCredentials(refreshedAccessToken); - const headers = { - Authorization: 'Bearer ' + this.credentials.access_token, - }; + const headers = new Headers({ + authorization: 'Bearer ' + this.credentials.access_token, + }); return {headers: this.addSharedMetadataHeaders(headers)}; } } if (this.apiKey) { - return {headers: {'X-Goog-Api-Key': this.apiKey}}; + return {headers: new Headers({'X-Goog-Api-Key': this.apiKey})}; } let r: GetTokenResponse | null = null; let tokens: Credentials | null = null; @@ -997,9 +994,9 @@ export class OAuth2Client extends AuthClient { credentials.token_type = credentials.token_type || 'Bearer'; tokens.refresh_token = credentials.refresh_token; this.credentials = tokens; - const headers: {[index: string]: string} = { - Authorization: credentials.token_type + ' ' + tokens.access_token, - }; + const headers = new Headers({ + authorization: credentials.token_type + ' ' + tokens.access_token, + }); return {headers: this.addSharedMetadataHeaders(headers), res: r.res}; } @@ -1112,20 +1109,17 @@ export class OAuth2Client extends AuthClient { opts: GaxiosOptions, reAuthRetried = false ): Promise> { - let r2: GaxiosResponse; try { const r = await this.getRequestMetadataAsync(opts.url); - opts.headers = opts.headers || {}; - if (r.headers && r.headers['x-goog-user-project']) { - opts.headers['x-goog-user-project'] = r.headers['x-goog-user-project']; - } - if (r.headers && r.headers.Authorization) { - opts.headers.Authorization = r.headers.Authorization; - } + opts.headers = Gaxios.mergeHeaders(opts.headers); + + this.addUserProjectAndAuthHeaders(opts.headers, r.headers); + if (this.apiKey) { - opts.headers['X-Goog-Api-Key'] = this.apiKey; + opts.headers.set('X-Goog-Api-Key', this.apiKey); } - r2 = await this.transporter.request(opts); + + return await this.transporter.request(opts); } catch (e) { const res = (e as GaxiosError).response; if (res) { @@ -1188,7 +1182,6 @@ export class OAuth2Client extends AuthClient { } throw e; } - return r2; } /** @@ -1251,8 +1244,8 @@ export class OAuth2Client extends AuthClient { ...OAuth2Client.RETRY_CONFIG, method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Bearer ${accessToken}`, + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + authorization: `Bearer ${accessToken}`, }, url: this.endpoints.tokenInfoUrl.toString(), }); @@ -1326,14 +1319,14 @@ export class OAuth2Client extends AuthClient { throw e; } - const cacheControl = res ? res.headers['cache-control'] : undefined; + const cacheControl = res?.headers.get('cache-control'); let cacheAge = -1; if (cacheControl) { - const pattern = new RegExp('max-age=([0-9]*)'); - const regexResult = pattern.exec(cacheControl as string); - if (regexResult && regexResult.length === 2) { + const maxAge = /max-age=(?[0-9]+)/.exec(cacheControl)?.groups + ?.maxAge; + if (maxAge) { // Cache results with max-age (in seconds) - cacheAge = Number(regexResult[1]) * 1000; // milliseconds + cacheAge = Number(maxAge) * 1000; // milliseconds } } diff --git a/src/auth/oauth2common.ts b/src/auth/oauth2common.ts index 06c37522..c43ff78a 100644 --- a/src/auth/oauth2common.ts +++ b/src/auth/oauth2common.ts @@ -13,7 +13,6 @@ // limitations under the License. import {Gaxios, GaxiosOptions} from 'gaxios'; -import * as querystring from 'querystring'; import {createCrypto} from '../crypto/crypto'; @@ -110,6 +109,8 @@ export abstract class OAuthClientAuthHandler { opts: GaxiosOptions, bearerToken?: string ) { + opts.headers = Gaxios.mergeHeaders(opts.headers); + // Inject authenticated header. this.injectAuthenticatedHeaders(opts, bearerToken); // Inject authenticated request body. @@ -133,19 +134,18 @@ export abstract class OAuthClientAuthHandler { ) { // Bearer token prioritized higher than basic Auth. if (bearerToken) { - opts.headers = opts.headers || {}; - Object.assign(opts.headers, { - Authorization: `Bearer ${bearerToken}}`, + opts.headers = Gaxios.mergeHeaders(opts.headers, { + authorization: `Bearer ${bearerToken}`, }); } else if (this.#clientAuthentication?.confidentialClientType === 'basic') { - opts.headers = opts.headers || {}; + opts.headers = Gaxios.mergeHeaders(opts.headers); const clientId = this.#clientAuthentication!.clientId; const clientSecret = this.#clientAuthentication!.clientSecret || ''; const base64EncodedCreds = this.#crypto.encodeBase64StringUtf8( `${clientId}:${clientSecret}` ); - Object.assign(opts.headers, { - Authorization: `Basic ${base64EncodedCreds}`, + Gaxios.mergeHeaders(opts.headers, { + authorization: `Basic ${base64EncodedCreds}`, }); } } @@ -160,45 +160,44 @@ export abstract class OAuthClientAuthHandler { private injectAuthenticatedRequestBody(opts: GaxiosOptions) { if (this.#clientAuthentication?.confidentialClientType === 'request-body') { const method = (opts.method || 'GET').toUpperCase(); - // Inject authenticated request body. - if (METHODS_SUPPORTING_REQUEST_BODY.indexOf(method) !== -1) { - // Get content-type. - let contentType; - const headers = opts.headers || {}; - for (const key in headers) { - if (key.toLowerCase() === 'content-type' && headers[key]) { - contentType = headers[key].toLowerCase(); - break; - } - } - if (contentType === 'application/x-www-form-urlencoded') { - opts.data = opts.data || ''; - const data = querystring.parse(opts.data); - Object.assign(data, { - client_id: this.#clientAuthentication!.clientId, - client_secret: this.#clientAuthentication!.clientSecret || '', - }); - opts.data = querystring.stringify(data); - } else if (contentType === 'application/json') { - opts.data = opts.data || {}; - Object.assign(opts.data, { - client_id: this.#clientAuthentication!.clientId, - client_secret: this.#clientAuthentication!.clientSecret || '', - }); - } else { - throw new Error( - `${contentType} content-types are not supported with ` + - `${this.#clientAuthentication!.confidentialClientType} ` + - 'client authentication' - ); - } - } else { + + if (!METHODS_SUPPORTING_REQUEST_BODY.includes(method)) { throw new Error( `${method} HTTP method does not support ` + `${this.#clientAuthentication!.confidentialClientType} ` + 'client authentication' ); } + + // Get content-type + const headers = new Headers(opts.headers); + const contentType = headers.get('content-type'); + + // Inject authenticated request body + if ( + contentType?.startsWith('application/x-www-form-urlencoded') || + opts.data instanceof URLSearchParams + ) { + const data = new URLSearchParams(opts.data ?? ''); + data.append('client_id', this.#clientAuthentication!.clientId); + data.append( + 'client_secret', + this.#clientAuthentication!.clientSecret || '' + ); + opts.data = data; + } else if (contentType?.startsWith('application/json')) { + opts.data = opts.data || {}; + Object.assign(opts.data, { + client_id: this.#clientAuthentication!.clientId, + client_secret: this.#clientAuthentication!.clientSecret || '', + }); + } else { + throw new Error( + `${contentType} content-types are not supported with ` + + `${this.#clientAuthentication!.confidentialClientType} ` + + 'client authentication' + ); + } } } diff --git a/src/auth/passthrough.ts b/src/auth/passthrough.ts index 6d6b7c17..a1515194 100644 --- a/src/auth/passthrough.ts +++ b/src/auth/passthrough.ts @@ -13,7 +13,7 @@ // limitations under the License. import {GaxiosOptions} from 'gaxios'; -import {AuthClient, GetAccessTokenResponse, Headers} from './authclient'; +import {AuthClient, GetAccessTokenResponse} from './authclient'; /** * An AuthClient without any Authentication information. Useful for: @@ -55,7 +55,7 @@ export class PassThroughClient extends AuthClient { * @returns {} */ async getRequestHeaders(): Promise { - return {}; + return new Headers(); } } diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 525c5412..48ad7dea 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -103,17 +103,14 @@ export class UserRefreshClient extends OAuth2Client { const res = await this.transporter.request({ ...UserRefreshClient.RETRY_CONFIG, url: this.endpoints.oauth2TokenUrl, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, method: 'POST', - data: stringify({ + data: new URLSearchParams({ client_id: this._clientId, client_secret: this._clientSecret, grant_type: 'refresh_token', refresh_token: this._refreshToken, target_audience: targetAudience, - }), + } as {}), }); return res.data.id_token!; diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 72bfceb5..fde2704e 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -13,8 +13,7 @@ // limitations under the License. import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; -import * as querystring from 'querystring'; -import {Headers} from './authclient'; +import {HeadersInit} from './authclient'; import { ClientAuthentication, OAuthClientAuthHandler, @@ -109,6 +108,7 @@ interface StsRequestOptions { client_secret?: string; // GCP-specific non-standard field. options?: string; + [key: string]: string | undefined; } /** @@ -186,9 +186,8 @@ export class StsCredentials extends OAuthClientAuthHandler { */ async exchangeToken( stsCredentialsOptions: StsCredentialsOptions, - additionalHeaders?: Headers, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options?: {[key: string]: any} + headers?: HeadersInit, + options?: Parameters[0] ): Promise { const values: StsRequestOptions = { grant_type: stsCredentialsOptions.grantType, @@ -203,30 +202,21 @@ export class StsCredentials extends OAuthClientAuthHandler { // Non-standard GCP-specific options. options: options && JSON.stringify(options), }; - // Remove undefined fields. - Object.keys(values).forEach(key => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (typeof (values as {[index: string]: any})[key] === 'undefined') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (values as {[index: string]: any})[key]; + + // Keep defined fields. + const payload: Record = {}; + Object.entries(values).forEach(([key, value]) => { + if (value !== undefined) { + payload[key] = value; } }); - const headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - }; - // Inject additional STS headers if available. - Object.assign(headers, additionalHeaders || {}); - const opts: GaxiosOptions = { ...StsCredentials.RETRY_CONFIG, url: this.#tokenExchangeEndpoint.toString(), method: 'POST', headers, - data: querystring.stringify( - values as unknown as querystring.ParsedUrlQueryInput - ), - responseType: 'json', + data: new URLSearchParams(payload), }; // Apply OAuth client authentication. this.applyClientAuthenticationOptions(opts); diff --git a/src/auth/urlsubjecttokensupplier.ts b/src/auth/urlsubjecttokensupplier.ts index 9ae01f75..cc9b5a18 100644 --- a/src/auth/urlsubjecttokensupplier.ts +++ b/src/auth/urlsubjecttokensupplier.ts @@ -87,7 +87,6 @@ export class UrlSubjectTokenSupplier implements SubjectTokenSupplier { url: this.url, method: 'GET', headers: this.headers, - responseType: this.formatType, }; let subjectToken: string | undefined; if (this.formatType === 'text') { diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 4e13bafa..0f6f2f3d 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -14,7 +14,6 @@ import * as assert from 'assert'; import * as nock from 'nock'; -import * as qs from 'querystring'; import {GetAccessTokenResponse} from '../src/auth/authclient'; import {OAuthErrorResponse} from '../src/auth/oauth2common'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; @@ -66,14 +65,14 @@ export function mockStsTokenExchange( ): nock.Scope { const headers = Object.assign( { - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, additionalHeaders || {} ); const scope = nock(baseURL, {reqheaders: headers}); nockParams.forEach(nockMockStsToken => { scope - .post(path, qs.stringify(nockMockStsToken.request)) + .post(path, nockMockStsToken.request) .reply(nockMockStsToken.statusCode, nockMockStsToken.response); }); return scope; @@ -85,8 +84,8 @@ export function mockGenerateAccessToken( const token = nockMockGenerateAccessToken.token; const scope = nock(saBaseUrl, { reqheaders: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', + authorization: `Bearer ${token}`, + 'content-type': 'application/json', }, }); scope @@ -131,7 +130,7 @@ export function mockCloudResourceManager( response: ProjectInfo | CloudRequestError ): nock.Scope { return nock('https://cloudresourcemanager.googleapis.com', { - reqheaders: {Authorization: `Bearer ${accessToken}`}, + reqheaders: {authorization: `Bearer ${accessToken}`}, }) .get(`/v1/projects/${projectNumber}`) .reply(statusCode, response); diff --git a/test/test.authclient.ts b/test/test.authclient.ts index 2150bb4a..4674cf3b 100644 --- a/test/test.authclient.ts +++ b/test/test.authclient.ts @@ -14,7 +14,7 @@ import {strict as assert} from 'assert'; -import {Gaxios, GaxiosOptions} from 'gaxios'; +import {Gaxios, GaxiosOptionsPrepared} from 'gaxios'; import {AuthClient, PassThroughClient} from '../src'; import {snakeToCamel} from '../src/util'; @@ -82,64 +82,73 @@ describe('AuthClient', () => { describe('User-Agent', () => { it('should set the header if it does not exist', async () => { - const options: GaxiosOptions = {}; + const options: GaxiosOptionsPrepared = { + headers: new Headers(), + url: new URL('https://google.com'), + }; await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); - assert.equal(options.headers?.['User-Agent'], USER_AGENT); + assert.equal(options.headers?.get('User-Agent'), USER_AGENT); }); it('should append to the header if it does exist and does not have the product name', async () => { const base = 'ABC XYZ'; const expected = `${base} ${USER_AGENT}`; - const options: GaxiosOptions = { - headers: { + const options: GaxiosOptionsPrepared = { + headers: new Headers({ 'User-Agent': base, - }, + }), + url: new URL('https://google.com'), }; await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); - assert.equal(options.headers?.['User-Agent'], expected); + assert.equal(options.headers.get('User-Agent'), expected); }); it('should not append to the header if it does exist and does have the product name', async () => { const expected = `ABC ${PRODUCT_NAME}/XYZ`; - const options: GaxiosOptions = { - headers: { + const options: GaxiosOptionsPrepared = { + headers: new Headers({ 'User-Agent': expected, - }, + }), + url: new URL('https://google.com'), }; await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); - assert.equal(options.headers?.['User-Agent'], expected); + assert.equal(options.headers.get('User-Agent'), expected); }); }); describe('x-goog-api-client', () => { it('should set the header if it does not exist', async () => { - const options: GaxiosOptions = {}; + const options: GaxiosOptionsPrepared = { + headers: new Headers(), + url: new URL('https://google.com'), + }; await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); assert.equal( - options.headers?.['x-goog-api-client'], + options.headers.get('x-goog-api-client'), `gl-node/${process.version.replace(/^v/, '')}` ); }); it('should not overwrite an existing header', async () => { const expected = 'abc'; - const options: GaxiosOptions = { - headers: { + const options: GaxiosOptionsPrepared = { + headers: new Headers({ 'x-goog-api-client': expected, - }, + }), + url: new URL('https://google.com'), }; await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); - assert.equal(options.headers?.['x-goog-api-client'], expected); + assert.equal(options.headers.get('x-goog-api-client'), expected); }); }); }); diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index db05785b..9fbc847f 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -98,7 +98,7 @@ describe('AwsClient', () => { '?Action=GetCallerIdentity&Version=2011-06-15', method: 'POST', headers: { - Authorization: + authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + `${dateStamp}/${awsRegion}/sts/aws4_request, SignedHeaders=host;` + 'x-amz-date;x-amz-security-token, Signature=' + @@ -114,25 +114,25 @@ describe('AwsClient', () => { method: expectedSignedRequest.method, headers: [ { - key: 'x-goog-cloud-target-resource', - value: awsOptions.audience, - }, - { - key: 'x-amz-date', - value: expectedSignedRequest.headers['x-amz-date'], - }, - { - key: 'Authorization', - value: expectedSignedRequest.headers.Authorization, + key: 'authorization', + value: expectedSignedRequest.headers.authorization, }, { key: 'host', value: expectedSignedRequest.headers.host, }, + { + key: 'x-amz-date', + value: expectedSignedRequest.headers['x-amz-date'], + }, { key: 'x-amz-security-token', value: expectedSignedRequest.headers['x-amz-security-token'], }, + { + key: 'x-goog-cloud-target-resource', + value: awsOptions.audience, + }, ], }) ); @@ -144,7 +144,7 @@ describe('AwsClient', () => { '?Action=GetCallerIdentity&Version=2011-06-15', method: 'POST', headers: { - Authorization: + authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + `${dateStamp}/${awsRegion}/sts/aws4_request, SignedHeaders=host;` + 'x-amz-date, Signature=' + @@ -159,20 +159,20 @@ describe('AwsClient', () => { method: expectedSignedRequestNoToken.method, headers: [ { - key: 'x-goog-cloud-target-resource', - value: awsOptions.audience, + key: 'authorization', + value: expectedSignedRequestNoToken.headers.authorization, }, { - key: 'x-amz-date', - value: expectedSignedRequestNoToken.headers['x-amz-date'], + key: 'host', + value: expectedSignedRequestNoToken.headers.host, }, { - key: 'Authorization', - value: expectedSignedRequestNoToken.headers.Authorization, + key: 'x-amz-date', + value: expectedSignedRequestNoToken.headers['x-amz-date'], }, { - key: 'host', - value: expectedSignedRequestNoToken.headers.host, + key: 'x-goog-cloud-target-resource', + value: awsOptions.audience, }, ], }) @@ -509,7 +509,7 @@ describe('AwsClient', () => { }); it('should reject when "credential_source.region_url" is missing', async () => { - const expectedError = new Error( + const expectedError = new RangeError( 'Unable to determine AWS region due to missing ' + '"options.credential_source.region_url"' ); diff --git a/test/test.awsrequestsigner.ts b/test/test.awsrequestsigner.ts index ebe3824e..d675e4f8 100644 --- a/test/test.awsrequestsigner.ts +++ b/test/test.awsrequestsigner.ts @@ -122,13 +122,13 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com', method: 'GET', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }; }, }, @@ -147,9 +147,9 @@ describe('AwsRequestSigner', () => { originalRequest: { method: 'GET', url: 'https://host.foo.com/foo/bar/../..', - headers: { + headers: new Headers({ date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }, getSignedRequest: () => { const signature = @@ -157,13 +157,13 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/foo/bar/../..', method: 'GET', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }; }, }, @@ -191,13 +191,13 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/./', method: 'GET', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }; }, }, @@ -216,9 +216,9 @@ describe('AwsRequestSigner', () => { originalRequest: { method: 'GET', url: 'https://host.foo.com/./foo', - headers: { + headers: new Headers({ date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }, getSignedRequest: () => { const signature = @@ -226,13 +226,13 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/./foo', method: 'GET', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }; }, }, @@ -260,13 +260,13 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/%E1%88%B4', method: 'GET', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }; }, }, @@ -295,13 +295,13 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/?foo=Zoo&foo=aha', method: 'GET', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }; }, }, @@ -329,13 +329,13 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/?ሴ=bar', method: 'GET', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }; }, }, @@ -365,14 +365,14 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/', method: 'POST', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host;zoo, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - ZOO: 'zoobar', - }, + zoo: 'zoobar', + }), }; }, }, @@ -403,14 +403,14 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/', method: 'POST', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host;zoo, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', zoo: 'ZOOBAR', - }, + }), }; }, }, @@ -440,14 +440,14 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com', method: 'POST', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host;p, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', p: 'phfft', - }, + }), }; }, }, @@ -467,7 +467,7 @@ describe('AwsRequestSigner', () => { method: 'POST', url: 'https://host.foo.com', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded', date: 'Mon, 09 Sep 2011 23:36:00 GMT', }, body: 'foo=bar', @@ -478,15 +478,15 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com', method: 'POST', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + 'aws4_request, SignedHeaders=content-type;date;host, ' + `Signature=${signature}`, host: 'host.foo.com', - 'Content-Type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), body: 'foo=bar', }; }, @@ -516,13 +516,13 @@ describe('AwsRequestSigner', () => { return { url: 'https://host.foo.com/?foo=bar', method: 'POST', - headers: { - Authorization: + headers: new Headers({ + authorization: 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/' + `aws4_request, SignedHeaders=date;host, Signature=${signature}`, host: 'host.foo.com', date: 'Mon, 09 Sep 2011 23:36:00 GMT', - }, + }), }; }, }, @@ -545,15 +545,15 @@ describe('AwsRequestSigner', () => { 'https://ec2.us-east-2.amazonaws.com?' + 'Action=DescribeRegions&Version=2013-10-15', method: 'GET', - headers: { - Authorization: + headers: new Headers({ + authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + `${dateStamp}/us-east-2/ec2/aws4_request, SignedHeaders=host;` + `x-amz-date;x-amz-security-token, Signature=${signature}`, host: 'ec2.us-east-2.amazonaws.com', 'x-amz-date': amzDate, 'x-amz-security-token': token, - }, + }), }; }, }, @@ -577,15 +577,15 @@ describe('AwsRequestSigner', () => { 'https://sts.us-east-2.amazonaws.com' + '?Action=GetCallerIdentity&Version=2011-06-15', method: 'POST', - headers: { + headers: new Headers({ 'x-amz-date': amzDate, - Authorization: + authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + `${dateStamp}/us-east-2/sts/aws4_request, SignedHeaders=host;` + `x-amz-date;x-amz-security-token, Signature=${signature}`, host: 'sts.us-east-2.amazonaws.com', 'x-amz-security-token': token, - }, + }), }; }, }, @@ -609,14 +609,14 @@ describe('AwsRequestSigner', () => { 'https://sts.us-east-2.amazonaws.com' + '?Action=GetCallerIdentity&Version=2011-06-15', method: 'POST', - headers: { + headers: new Headers({ 'x-amz-date': amzDate, - Authorization: + authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + `${dateStamp}/us-east-2/sts/aws4_request, SignedHeaders=host;` + `x-amz-date, Signature=${signature}`, host: 'sts.us-east-2.amazonaws.com', - }, + }), }; }, }, @@ -628,7 +628,7 @@ describe('AwsRequestSigner', () => { url: 'https://dynamodb.us-east-2.amazonaws.com/', method: 'POST', headers: { - 'Content-Type': 'application/x-amz-json-1.0', + 'content-type': 'application/x-amz-json-1.0', 'x-amz-target': 'DynamoDB_20120810.CreateTable', }, body: JSON.stringify(requestParams), @@ -641,18 +641,18 @@ describe('AwsRequestSigner', () => { return { url: 'https://dynamodb.us-east-2.amazonaws.com/', method: 'POST', - headers: { - Authorization: + headers: new Headers({ + authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + `${dateStamp}/us-east-2/dynamodb/aws4_request, SignedHeaders=` + 'content-type;host;x-amz-date;x-amz-security-token;x-amz-target' + `, Signature=${signature}`, - 'Content-Type': 'application/x-amz-json-1.0', + 'content-type': 'application/x-amz-json-1.0', host: 'dynamodb.us-east-2.amazonaws.com', 'x-amz-date': amzDate, 'x-amz-security-token': token, 'x-amz-target': 'DynamoDB_20120810.CreateTable', - }, + }), body: JSON.stringify(requestParams), }; }, @@ -665,7 +665,7 @@ describe('AwsRequestSigner', () => { url: 'https://dynamodb.us-east-2.amazonaws.com/', method: 'POST', headers: { - 'Content-Type': 'application/x-amz-json-1.0', + 'content-type': 'application/x-amz-json-1.0', 'x-amz-target': 'DynamoDB_20120810.CreateTable', }, data: requestParams, @@ -678,18 +678,18 @@ describe('AwsRequestSigner', () => { return { url: 'https://dynamodb.us-east-2.amazonaws.com/', method: 'POST', - headers: { - Authorization: + headers: new Headers({ + authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/` + `${dateStamp}/us-east-2/dynamodb/aws4_request, SignedHeaders=` + 'content-type;host;x-amz-date;x-amz-security-token;x-amz-target' + `, Signature=${signature}`, - 'Content-Type': 'application/x-amz-json-1.0', + 'content-type': 'application/x-amz-json-1.0', host: 'dynamodb.us-east-2.amazonaws.com', 'x-amz-date': amzDate, 'x-amz-security-token': token, 'x-amz-target': 'DynamoDB_20120810.CreateTable', - }, + }), body: JSON.stringify(requestParams), }; }, @@ -725,7 +725,7 @@ describe('AwsRequestSigner', () => { }); it('should reject when no URL is available', async () => { - const invalidOptionsError = new Error( + const invalidOptionsError = new RangeError( '"url" is required in "amzOptions"' ); const awsRequestSigner = new AwsRequestSigner( diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index ad9743d2..34229748 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -697,7 +697,7 @@ describe('BaseExternalAccountClient', () => { }, ], { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( + authorization: `Basic ${crypto.encodeBase64StringUtf8( basicAuthCreds )}`, } @@ -772,7 +772,7 @@ describe('BaseExternalAccountClient', () => { }, ], { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( + authorization: `Basic ${crypto.encodeBase64StringUtf8( basicAuthCreds )}`, } @@ -1153,7 +1153,7 @@ describe('BaseExternalAccountClient', () => { }, ], { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( + authorization: `Basic ${crypto.encodeBase64StringUtf8( basicAuthCreds )}`, } @@ -1626,7 +1626,7 @@ describe('BaseExternalAccountClient', () => { }, ], { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( + authorization: `Basic ${crypto.encodeBase64StringUtf8( basicAuthCreds )}`, } @@ -1870,9 +1870,9 @@ describe('BaseExternalAccountClient', () => { describe('getRequestHeaders()', () => { it('should inject the authorization headers', async () => { - const expectedHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, - }; + const expectedHeaders = new Headers({ + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }); const scope = mockStsTokenExchange([ { statusCode: 200, @@ -1902,9 +1902,9 @@ describe('BaseExternalAccountClient', () => { accessToken: 'SA_ACCESS_TOKEN', expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(), }; - const expectedHeaders = { - Authorization: `Bearer ${saSuccessResponse.accessToken}`, - }; + const expectedHeaders = new Headers({ + authorization: `Bearer ${saSuccessResponse.accessToken}`, + }); const scopes: nock.Scope[] = []; scopes.push( mockStsTokenExchange([ @@ -1943,10 +1943,10 @@ describe('BaseExternalAccountClient', () => { it('should inject the authorization and metadata headers', async () => { const quotaProjectId = 'QUOTA_PROJECT_ID'; - const expectedHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + const expectedHeaders = new Headers({ + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, 'x-goog-user-project': quotaProjectId, - }; + }); const scope = mockStsTokenExchange([ { statusCode: 200, @@ -2009,7 +2009,7 @@ describe('BaseExternalAccountClient', () => { it('should process HTTP request with authorization header', async () => { const quotaProjectId = 'QUOTA_PROJECT_ID'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, 'x-goog-user-project': quotaProjectId, }; const optionsWithQuotaProjectId = Object.assign( @@ -2057,7 +2057,6 @@ describe('BaseExternalAccountClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -2072,7 +2071,7 @@ describe('BaseExternalAccountClient', () => { }; const quotaProjectId = 'QUOTA_PROJECT_ID'; const authHeaders = { - Authorization: `Bearer ${saSuccessResponse.accessToken}`, + authorization: `Bearer ${saSuccessResponse.accessToken}`, 'x-goog-user-project': quotaProjectId, }; const optionsWithQuotaProjectId = Object.assign( @@ -2126,7 +2125,6 @@ describe('BaseExternalAccountClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -2136,7 +2134,7 @@ describe('BaseExternalAccountClient', () => { it('should process headerless HTTP request', async () => { const quotaProjectId = 'QUOTA_PROJECT_ID'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, 'x-goog-user-project': quotaProjectId, }; const optionsWithQuotaProjectId = Object.assign( @@ -2180,7 +2178,6 @@ describe('BaseExternalAccountClient', () => { url: 'https://example.com/api', method: 'POST', data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -2219,7 +2216,6 @@ describe('BaseExternalAccountClient', () => { url: 'https://example.com/api', method: 'POST', data: exampleRequest, - responseType: 'json', }), getErrorFromOAuthErrorResponse(errorResponse) ); @@ -2228,7 +2224,7 @@ describe('BaseExternalAccountClient', () => { it('should trigger callback on success when provided', done => { const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -2272,7 +2268,6 @@ describe('BaseExternalAccountClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }, (err, result) => { assert.strictEqual(err, null); @@ -2286,7 +2281,7 @@ describe('BaseExternalAccountClient', () => { it('should trigger callback on error when provided', done => { const errorMessage = 'Bad Request'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -2326,7 +2321,6 @@ describe('BaseExternalAccountClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }, err => { assert(err instanceof GaxiosError); @@ -2342,10 +2336,10 @@ describe('BaseExternalAccountClient', () => { const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const authHeaders2 = { - Authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -2409,7 +2403,6 @@ describe('BaseExternalAccountClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -2418,7 +2411,7 @@ describe('BaseExternalAccountClient', () => { it('should not retry on 401 on forceRefreshOnFailure=false', async () => { const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -2458,7 +2451,6 @@ describe('BaseExternalAccountClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }), { status: 401, @@ -2472,10 +2464,10 @@ describe('BaseExternalAccountClient', () => { const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const authHeaders2 = { - Authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -2536,7 +2528,6 @@ describe('BaseExternalAccountClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }), { status: 403, diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index 563bb758..43e0717f 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -31,7 +31,7 @@ import { OAuthErrorResponse, getErrorFromOAuthErrorResponse, } from '../src/auth/oauth2common'; -import {GetAccessTokenResponse, Headers} from '../src/auth/authclient'; +import {GetAccessTokenResponse} from '../src/auth/authclient'; nock.disableNetConnect(); @@ -715,9 +715,9 @@ describe('DownscopedClient', () => { describe('getRequestHeader()', () => { it('should inject the authorization headers', async () => { - const expectedHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, - }; + const expectedHeaders = new Headers({ + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }); const scope = mockStsTokenExchange([ { statusCode: 200, @@ -742,10 +742,10 @@ describe('DownscopedClient', () => { it('should inject the authorization and metadata headers', async () => { const quotaProjectId = 'QUOTA_PROJECT_ID'; - const expectedHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + const expectedHeaders = new Headers({ + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, 'x-goog-user-project': quotaProjectId, - }; + }); const scope = mockStsTokenExchange([ { statusCode: 200, @@ -803,7 +803,7 @@ describe('DownscopedClient', () => { it('should process HTTP request with authorization header', async () => { const quotaProjectId = 'QUOTA_PROJECT_ID'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, 'x-goog-user-project': quotaProjectId, }; const exampleRequest = { @@ -848,7 +848,6 @@ describe('DownscopedClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -858,7 +857,7 @@ describe('DownscopedClient', () => { it('should process headerless HTTP request', async () => { const quotaProjectId = 'QUOTA_PROJECT_ID'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, 'x-goog-user-project': quotaProjectId, }; const exampleRequest = { @@ -899,7 +898,6 @@ describe('DownscopedClient', () => { url: 'https://example.com/api', method: 'POST', data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -937,7 +935,6 @@ describe('DownscopedClient', () => { url: 'https://example.com/api', method: 'POST', data: exampleRequest, - responseType: 'json', }), getErrorFromOAuthErrorResponse(errorResponse) ); @@ -946,7 +943,7 @@ describe('DownscopedClient', () => { it('should trigger callback on success when provided', done => { const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -990,7 +987,6 @@ describe('DownscopedClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }, (err, result) => { assert.strictEqual(err, null); @@ -1004,7 +1000,7 @@ describe('DownscopedClient', () => { it('should trigger callback on error when provided', done => { const errorMessage = 'Bad Request'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -1044,7 +1040,6 @@ describe('DownscopedClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }, err => { assert(err instanceof GaxiosError); @@ -1060,10 +1055,10 @@ describe('DownscopedClient', () => { const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'DOWNSCOPED_CLIENT_ACCESS_TOKEN_1'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const authHeaders2 = { - Authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -1125,7 +1120,6 @@ describe('DownscopedClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -1134,7 +1128,7 @@ describe('DownscopedClient', () => { it('should not retry on 401 on forceRefreshOnFailure=false', async () => { const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -1175,7 +1169,6 @@ describe('DownscopedClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }), { status: 401, @@ -1189,10 +1182,10 @@ describe('DownscopedClient', () => { const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'DOWNSCOPED_CLIENT_ACCESS_TOKEN_1'; const authHeaders = { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }; const authHeaders2 = { - Authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse2.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -1251,7 +1244,6 @@ describe('DownscopedClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }), { status: 403, diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 3f86856d..f13de319 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -62,7 +62,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ): nock.Scope { const headers = Object.assign( { - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, additionalHeaders || {} ); @@ -275,19 +275,19 @@ describe('ExternalAccountAuthorizedUserClient', () => { // we need timers/`setTimeout` for this test clock.restore(); - const expectedRequest = new URLSearchParams({ + const expectedRequest = { grant_type: 'refresh_token', refresh_token: 'refreshToken', - }); + }; const scope = nock(BASE_URL, { reqheaders: { - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, }) - .post(REFRESH_PATH, expectedRequest.toString()) - .replyWithError({code: 'ETIMEDOUT'}) - .post(REFRESH_PATH, expectedRequest.toString()) + .post(REFRESH_PATH, expectedRequest) + .replyWithError('ETIMEOUT') + .post(REFRESH_PATH, expectedRequest) .reply(200, successfulRefreshResponse); const client = new ExternalAccountAuthorizedUserClient( @@ -414,10 +414,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { describe('getRequestHeaders()', () => { it('should inject the authorization headers', async () => { - const expectedHeaders = { - Authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, + const expectedHeaders = new Headers({ + authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, 'x-goog-user-project': 'quotaProjectId', - }; + }); const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ { statusCode: 200, @@ -474,7 +474,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { it('should process HTTP request with authorization header', async () => { const quotaProjectId = 'QUOTA_PROJECT_ID'; const authHeaders = { - Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + authorization: `Bearer ${successfulRefreshResponse.access_token}`, 'x-goog-user-project': quotaProjectId, }; const optionsWithQuotaProjectId = Object.assign( @@ -519,7 +519,6 @@ describe('ExternalAccountAuthorizedUserClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -555,7 +554,6 @@ describe('ExternalAccountAuthorizedUserClient', () => { url: 'https://example.com/api', method: 'POST', data: exampleRequest, - responseType: 'json', }), getErrorFromOAuthErrorResponse(errorResponse) ); @@ -564,7 +562,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { it('should trigger callback on success when provided', done => { const authHeaders = { - Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + authorization: `Bearer ${successfulRefreshResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -605,7 +603,6 @@ describe('ExternalAccountAuthorizedUserClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }, (err, result) => { assert.strictEqual(err, null); @@ -619,7 +616,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { it('should trigger callback on error when provided', done => { const errorMessage = 'Bad Request'; const authHeaders = { - Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + authorization: `Bearer ${successfulRefreshResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -656,7 +653,6 @@ describe('ExternalAccountAuthorizedUserClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }, err => { assert(err instanceof GaxiosError); @@ -670,7 +666,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { it('should retry on 401 on forceRefreshOnFailure=true', async () => { const authHeaders = { - Authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, + authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -717,7 +713,6 @@ describe('ExternalAccountAuthorizedUserClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); @@ -726,7 +721,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { it('should not retry on 401 on forceRefreshOnFailure=false', async () => { const authHeaders = { - Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + authorization: `Bearer ${successfulRefreshResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -764,7 +759,6 @@ describe('ExternalAccountAuthorizedUserClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }), { status: 401, @@ -776,7 +770,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { it('should not retry more than once', async () => { const authHeaders = { - Authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, + authorization: `Bearer ${successfulRefreshResponseNoRefreshToken.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -820,7 +814,6 @@ describe('ExternalAccountAuthorizedUserClient', () => { method: 'POST', headers: exampleHeaders, data: exampleRequest, - responseType: 'json', }), { status: 403, @@ -831,7 +824,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { it('should process headerless HTTP request', async () => { const authHeaders = { - Authorization: `Bearer ${successfulRefreshResponse.access_token}`, + authorization: `Bearer ${successfulRefreshResponse.access_token}`, }; const exampleRequest = { key1: 'value1', @@ -867,7 +860,6 @@ describe('ExternalAccountAuthorizedUserClient', () => { url: 'https://example.com/api', method: 'POST', data: exampleRequest, - responseType: 'json', }); assert.deepStrictEqual(actualResponse.data, exampleResponse); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 99e6b03c..73537d91 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -62,7 +62,7 @@ import {stringify} from 'querystring'; import {GoogleAuthExceptionMessages} from '../src/auth/googleauth'; import {IMPERSONATED_ACCOUNT_TYPE} from '../src/auth/impersonated'; import {USER_REFRESH_ACCOUNT_TYPE} from '../src/auth/refreshclient'; -import {GaxiosError} from 'gaxios'; +import {Gaxios, GaxiosError} from 'gaxios'; nock.disableNetConnect(); @@ -111,8 +111,8 @@ describe('googleauth', () => { 'application_default_credentials.json' ); function createGTokenMock(body: CredentialRequest) { - return nock('https://www.googleapis.com') - .post('/oauth2/v4/token') + return nock('https://oauth2.googleapis.com') + .post('/token') .reply(200, body); } @@ -284,9 +284,9 @@ describe('googleauth', () => { } it('should accept and use an `AuthClient`', async () => { - const customRequestHeaders = { + const customRequestHeaders = new Headers({ 'my-unique': 'header', - }; + }); // Using a custom `AuthClient` to ensure any `AuthClient` would work class MyAuthClient extends AuthClient { @@ -295,7 +295,7 @@ describe('googleauth', () => { } async getRequestHeaders() { - return {...customRequestHeaders}; + return Gaxios.mergeHeaders({...customRequestHeaders}); } request = OAuth2Client.prototype.request.bind(this); @@ -316,9 +316,12 @@ describe('googleauth', () => { const client = await auth.getClient(); assert.equal(client.apiKey, apiKey); - assert.deepEqual(await auth.getRequestHeaders(), { - 'X-Goog-Api-Key': apiKey, - }); + assert.deepEqual( + await auth.getRequestHeaders(), + new Headers({ + 'X-Goog-Api-Key': apiKey, + }) + ); }); it('should not accept both an `apiKey` and `credentials`', async () => { @@ -364,7 +367,7 @@ describe('googleauth', () => { const scope = nock(BASE_URL) .post(ENDPOINT) .reply(function () { - assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); + assert.strictEqual(this.req.headers['x-goog-api-key'], API_KEY); return [200, RESPONSE_BODY]; }); const client = auth.fromAPIKey(API_KEY); @@ -380,7 +383,7 @@ describe('googleauth', () => { it('should put the api key in the headers', async () => { const client = auth.fromAPIKey(API_KEY); const headers = await client.getRequestHeaders(); - assert.strictEqual(headers['X-Goog-Api-Key'], API_KEY); + assert.strictEqual(headers.get('X-Goog-Api-Key'), API_KEY); }); it('should make a request while preserving original parameters', async () => { @@ -389,7 +392,7 @@ describe('googleauth', () => { .post(ENDPOINT) .query({test: OTHER_QS_PARAM.test}) .reply(function (uri) { - assert.strictEqual(this.req.headers['x-goog-api-key'][0], API_KEY); + assert.strictEqual(this.req.headers['x-goog-api-key'], API_KEY); assert(uri.indexOf('test=' + OTHER_QS_PARAM.test) > -1); return [200, RESPONSE_BODY]; }); @@ -1312,7 +1315,10 @@ describe('googleauth', () => { scopes.push(createGetProjectIdNock()); const headers = await auth.getRequestHeaders(); scopes.forEach(s => s.done()); - assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); + assert.deepStrictEqual( + headers, + new Headers({authorization: 'Bearer abc123'}) + ); }); it('should authorize the request', async () => { @@ -1320,7 +1326,10 @@ describe('googleauth', () => { scopes.push(createGetProjectIdNock()); const opts = await auth.authorizeRequest({url: 'http://example.com'}); scopes.forEach(s => s.done()); - assert.deepStrictEqual(opts.headers, {Authorization: 'Bearer abc123'}); + assert.deepStrictEqual( + opts.headers, + new Headers({authorization: 'Bearer abc123'}) + ); }); it('should get the current environment if GCE', async () => { @@ -1470,7 +1479,10 @@ describe('googleauth', () => { ); const auth = new GoogleAuth(); const headers = await auth.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + assert.strictEqual( + headers.get('x-goog-user-project'), + 'my-quota-project' + ); tokenReq.done(); }); @@ -1480,7 +1492,7 @@ describe('googleauth', () => { ); const auth = new GoogleAuth(); const headers = await auth.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], undefined); + assert.strictEqual(headers.get('x-goog-user-project'), null); tokenReq.done(); }); @@ -1492,7 +1504,10 @@ describe('googleauth', () => { const client = await auth.getClient(); assert(client instanceof UserRefreshClient); const headers = await client.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + assert.strictEqual( + headers.get('x-goog-user-project'), + 'my-quota-project' + ); tokenReq.done(); }); @@ -1507,7 +1522,7 @@ describe('googleauth', () => { .post(ENDPOINT) .reply(function () { assert.strictEqual( - this.req.headers['x-goog-user-project'][0], + this.req.headers['x-goog-user-project'], 'my-quota-project' ); return [200, RESPONSE_BODY]; @@ -1783,8 +1798,8 @@ describe('googleauth', () => { }, { reqheaders: { - Authorization: `Bearer ${saSuccessResponse.accessToken}`, - 'Content-Type': 'application/json', + authorization: `Bearer ${saSuccessResponse.accessToken}`, + 'content-type': 'application/json', }, } ) @@ -2478,8 +2493,8 @@ describe('googleauth', () => { }, { reqheaders: { - Authorization: `Bearer ${saSuccessResponse.accessToken}`, - 'Content-Type': 'application/json', + authorization: `Bearer ${saSuccessResponse.accessToken}`, + 'content-type': 'application/json', }, } ) @@ -2525,9 +2540,12 @@ describe('googleauth', () => { const auth = new GoogleAuth({keyFilename}); const headers = await auth.getRequestHeaders(); - assert.deepStrictEqual(headers, { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, - }); + assert.deepStrictEqual( + headers, + new Headers({ + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }) + ); scopes.forEach(s => s.done()); }); @@ -2537,9 +2555,12 @@ describe('googleauth', () => { const auth = new GoogleAuth({keyFilename}); const opts = await auth.authorizeRequest({url: 'http://example.com'}); - assert.deepStrictEqual(opts.headers, { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, - }); + assert.deepStrictEqual( + opts.headers, + new Headers({ + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }) + ); scopes.forEach(s => s.done()); }); @@ -2552,7 +2573,7 @@ describe('googleauth', () => { nock(url) .get('/', undefined, { reqheaders: { - Authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }, }) .reply(200, data) @@ -2729,7 +2750,7 @@ describe('googleauth', () => { const scope = createGTokenMock({access_token: 'initial-access-token'}); const headers = await jwt.getRequestHeaders(); assert.deepStrictEqual( - headers.Authorization, + headers.get('authorization'), 'Bearer initial-access-token' ); scope.done(); @@ -2751,7 +2772,7 @@ describe('googleauth', () => { const scope = createGTokenMock({access_token: 'initial-access-token'}); const headers = await jwt.getRequestHeaders(); assert.deepStrictEqual( - headers.Authorization, + headers.get('authorization'), 'Bearer initial-access-token' ); scope.done(); @@ -2773,7 +2794,7 @@ describe('googleauth', () => { const scope = createGTokenMock({access_token: 'initial-access-token'}); const headers = await jwt.getRequestHeaders(); assert.deepStrictEqual( - headers.Authorization, + headers.get('authorization'), 'Bearer initial-access-token' ); scope.done(); diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index ac9317c7..2f05d1b1 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -521,7 +521,7 @@ describe('IdentityPoolClient', () => { }, ], { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( + authorization: `Basic ${crypto.encodeBase64StringUtf8( basicAuthCreds )}`, } @@ -597,7 +597,7 @@ describe('IdentityPoolClient', () => { }, ], { - Authorization: `Basic ${crypto.encodeBase64StringUtf8( + authorization: `Basic ${crypto.encodeBase64StringUtf8( basicAuthCreds )}`, } diff --git a/test/test.idtokenclient.ts b/test/test.idtokenclient.ts index 8dabdc80..b11332e3 100644 --- a/test/test.idtokenclient.ts +++ b/test/test.idtokenclient.ts @@ -26,8 +26,8 @@ describe('idtokenclient', () => { nock.disableNetConnect(); function createGTokenMock(body: CredentialRequest) { - return nock('https://www.googleapis.com') - .post('/oauth2/v4/token') + return nock('https://oauth2.googleapis.com') + .post('/token') .reply(200, body); } @@ -68,7 +68,10 @@ describe('idtokenclient', () => { const headers = await client.getRequestHeaders(); scope.done(); assert.strictEqual(client.credentials.id_token, 'abc123'); - assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); + assert.deepStrictEqual( + headers, + new Headers({authorization: 'Bearer abc123'}) + ); }); it('should refresh ID token if expiry_date not set', async () => { @@ -87,6 +90,9 @@ describe('idtokenclient', () => { const headers = await client.getRequestHeaders(); scope.done(); assert.strictEqual(client.credentials.id_token, 'abc123'); - assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); + assert.deepStrictEqual( + headers, + new Headers({authorization: 'Bearer abc123'}) + ); }); }); diff --git a/test/test.impersonated.ts b/test/test.impersonated.ts index a0dfcee2..375bb22c 100644 --- a/test/test.impersonated.ts +++ b/test/test.impersonated.ts @@ -27,9 +27,7 @@ nock.disableNetConnect(); const url = 'http://example.com'; function createGTokenMock(body: CredentialRequest) { - return nock('https://www.googleapis.com') - .post('/oauth2/v4/token') - .reply(200, body); + return nock('https://oauth2.googleapis.com').post('/token').reply(200, body); } function createSampleJWTClient() { @@ -378,7 +376,7 @@ describe('impersonated', () => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const scopes = [ - nock('https://www.googleapis.com').post('/oauth2/v4/token').reply(401), + nock('https://oauth2.googleapis.com').post('/token').reply(401), ]; const impersonated = new Impersonated({ @@ -427,7 +425,7 @@ describe('impersonated', () => { impersonated.credentials.access_token = 'initial-access-token'; impersonated.credentials.expiry_date = Date.now() - 10000; const headers = await impersonated.getRequestHeaders(); - assert.strictEqual(headers['Authorization'], 'Bearer qwerty345'); + assert.strictEqual(headers.get('authorization'), 'Bearer qwerty345'); assert.strictEqual( impersonated.credentials.expiry_date, tomorrow.getTime() diff --git a/test/test.index.ts b/test/test.index.ts index 0c5581d8..a22b8bde 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -37,5 +37,8 @@ describe('index', () => { assert(gal.BaseExternalAccountClient); assert(gal.DownscopedClient); assert(gal.Impersonated); + + assert(gal.gaxios); + assert(gal.gcpMetadata); }); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 6ed2e3a7..0d12729e 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -23,6 +23,10 @@ import {GoogleAuth, JWT} from '../src'; import {CredentialRequest, JWTInput} from '../src/auth/credentials'; import * as jwtaccess from '../src/auth/jwtaccess'; +function removeBearerFromAuthorizationHeader(headers: Headers): string { + return (headers.get('authorization') || '').replace('Bearer ', ''); +} + describe('jwt', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const keypair = require('keypair'); @@ -52,8 +56,8 @@ describe('jwt', () => { } function createGTokenMock(body: CredentialRequest) { - return nock('https://www.googleapis.com') - .post('/oauth2/v4/token') + return nock('https://oauth2.googleapis.com') + .post('/token') .reply(200, body); } @@ -181,8 +185,8 @@ describe('jwt', () => { scope.done(); assert.strictEqual( want, - headers.Authorization, - `the authorization header was wrong: ${headers.Authorization}` + headers.get('authorization'), + `the authorization header was wrong: ${headers.get('authorization')}` ); }); @@ -198,7 +202,7 @@ describe('jwt', () => { const testUri = 'http:/example.com/my_test_service'; const got = await jwt.getRequestHeaders(testUri); assert.notStrictEqual(null, got, 'the creds should be present'); - const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); + const decoded = jws.decode(removeBearerFromAuthorizationHeader(got)); assert(decoded); assert.strictEqual(decoded.header.alg, 'RS256'); assert.strictEqual(decoded.header.typ, 'JWT'); @@ -221,7 +225,7 @@ describe('jwt', () => { const testUri = 'http:/example.com/my_test_service'; const got = await jwt.getRequestHeaders(testUri); assert.notStrictEqual(null, got, 'the creds should be present'); - const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); + const decoded = jws.decode(removeBearerFromAuthorizationHeader(got)); assert(decoded); assert.deepStrictEqual(decoded.header, { alg: 'RS256', @@ -246,7 +250,7 @@ describe('jwt', () => { const testDefault = 'https://example.com/'; const got = await jwt.getRequestHeaders(testUri); assert.notStrictEqual(null, got, 'the creds should be present'); - const decoded = jws.decode(got.Authorization.replace('Bearer ', '')); + const decoded = jws.decode(removeBearerFromAuthorizationHeader(got)); assert(decoded); const payload = decoded.payload; assert.strictEqual(testDefault, payload.aud); @@ -268,7 +272,7 @@ describe('jwt', () => { const got = await jwt.getRequestHeaders(testUri); scope.done(); assert.notStrictEqual(null, got, 'the creds should be present'); - const decoded = got.Authorization.replace('Bearer ', ''); + const decoded = removeBearerFromAuthorizationHeader(got); assert.strictEqual(decoded, 'abc123'); }); @@ -749,7 +753,10 @@ describe('jwt', () => { const headers = await client.getRequestHeaders( 'http:/example.com/my_test_service' ); - assert.strictEqual(headers['x-goog-user-project'], 'fake-quota-project'); + assert.strictEqual( + headers.get('x-goog-user-project'), + 'fake-quota-project' + ); }); it('should return an ID token for fetchIdToken', async () => { @@ -795,7 +802,7 @@ describe('jwt', () => { subject: 'bar@subjectaccount.com', }); const headers = await jwt.getRequestHeaders(); - assert.deepStrictEqual(headers, {}); + assert.deepStrictEqual(headers, new Headers()); }); it('returns empty headers if: user scope = false, default scope = false, audience = falsy, useJWTACcessWithScope = truthy', async () => { @@ -807,11 +814,11 @@ describe('jwt', () => { }); jwt.useJWTAccessWithScope = true; const headers = await jwt.getRequestHeaders(); - assert.deepStrictEqual(headers, {}); + assert.deepStrictEqual(headers, new Headers()); }); it('signs JWT with audience if: user scope = false, default scope = false, audience = truthy, useJWTAccessWithScope = false', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -833,7 +840,7 @@ describe('jwt', () => { }); it('signs JWT with audience if: user scope = false, default scope = true, audience = truthy, useJWTAccessWithScope = false', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -855,7 +862,7 @@ describe('jwt', () => { }); it('signs JWT with audience if: user scope = false, default scope = no, audience = truthy, useJWTAccessWithScope = truthy', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -877,7 +884,7 @@ describe('jwt', () => { }); it('signs JWT with audience if: user scope = false, default scope = yes, audience = truthy, useJWTAccessWithScope = truthy', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -900,7 +907,7 @@ describe('jwt', () => { }); it('signs JWT with scopes if: user scope = true, default scope = false, audience = falsy, useJWTAccessWithScope = true', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -922,7 +929,7 @@ describe('jwt', () => { }); it('signs JWT with scopes if: user scope = false, default scope = true, audience = falsy, useJWTAccessWithScope = true', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -943,7 +950,7 @@ describe('jwt', () => { }); it('signs JWT with scopes if: user scope = true, default scope = true, audience = falsy, useJWTAccessWithScope = true', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -966,7 +973,7 @@ describe('jwt', () => { }); it('signs JWT with scopes if: user scope = true, default scope = false, audience = truthy, useJWTAccessWithScope = true', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -988,7 +995,7 @@ describe('jwt', () => { }); it('signs JWT with scopes if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -1011,7 +1018,7 @@ describe('jwt', () => { }); it('signs JWT with scopes if: user scope = true, default scope = true, audience = truthy, universeDomain = not default universe', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -1033,7 +1040,7 @@ describe('jwt', () => { }); it('signs JWT with scopes if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true, universeDomain = not default universe', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -1056,7 +1063,7 @@ describe('jwt', () => { }); it('throws on domain-wide delegation on non-default universe', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubGetRequestHeaders = sandbox.stub().returns(new Headers()); sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: stubGetRequestHeaders, }); @@ -1078,7 +1085,7 @@ describe('jwt', () => { it('does not use self signed JWT if target_audience provided', async () => { const JWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ - getRequestHeaders: sinon.stub().returns({}), + getRequestHeaders: sinon.stub().returns(new Headers()), }); const keys = keypair(512 /* bitsize of private key */); const jwt = new JWT({ @@ -1159,11 +1166,7 @@ describe('jwt', () => { const scope = createGTokenMock({access_token: wantedToken}); const headers = await jwt.getRequestHeaders(); scope.done(); - assert.strictEqual( - want, - headers.Authorization, - `the authorization header was wrong: ${headers.Authorization}` - ); + assert.strictEqual(headers.get('authorization'), want); }); it('calls oauth2api if: user scope = true, default scope = false, audience = falsy, useJWTAccessWithScope = false', async () => { @@ -1179,11 +1182,7 @@ describe('jwt', () => { const scope = createGTokenMock({access_token: wantedToken}); const headers = await jwt.getRequestHeaders(); scope.done(); - assert.strictEqual( - want, - headers.Authorization, - `the authorization header was wrong: ${headers.Authorization}` - ); + assert.strictEqual(headers.get('authorization'), want); }); it('calls oauth2api if: user scope = true, default scope = true, audience = falsy, useJWTAccessWithScope = false', async () => { @@ -1201,11 +1200,7 @@ describe('jwt', () => { const scope = createGTokenMock({access_token: wantedToken}); const headers = await jwt.getRequestHeaders(); scope.done(); - assert.strictEqual( - want, - headers.Authorization, - `the authorization header was wrong: ${headers.Authorization}` - ); + assert.strictEqual(headers.get('authorization'), want); }); it('calls oauth2api if: user scope = true, default scope = false, audience = truthy, useJWTAccessWithScope = false', async () => { @@ -1222,11 +1217,7 @@ describe('jwt', () => { const scope = createGTokenMock({access_token: wantedToken}); const headers = await jwt.getRequestHeaders(testUri); scope.done(); - assert.strictEqual( - want, - headers.Authorization, - `the authorization header was wrong: ${headers.Authorization}` - ); + assert.strictEqual(headers.get('authorization'), want); }); it('calls oauth2api if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = false', async () => { @@ -1245,11 +1236,7 @@ describe('jwt', () => { const scope = createGTokenMock({access_token: wantedToken}); const headers = await jwt.getRequestHeaders(testUri); scope.done(); - assert.strictEqual( - want, - headers.Authorization, - `the authorization header was wrong: ${headers.Authorization}` - ); + assert.strictEqual(headers.get('authorization'), want); }); }); }); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index d441e3b0..fc542017 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -44,11 +44,15 @@ describe('jwtaccess', () => { }); afterEach(() => sandbox.restore()); + function removeBearerFromAuthorizationHeader(headers: Headers): string { + return (headers.get('authorization') || '').replace('Bearer ', ''); + } + it('getRequestHeaders should create a signed JWT token as the access token', () => { const client = new JWTAccess(email, keys.private); const headers = client.getRequestHeaders(testUri); assert.notStrictEqual(null, headers, 'an creds object should be present'); - const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + const decoded = jws.decode(removeBearerFromAuthorizationHeader(headers)); assert(decoded); assert.deepStrictEqual(decoded.header, {alg: 'RS256', typ: 'JWT'}); const payload = decoded.payload; @@ -60,7 +64,7 @@ describe('jwtaccess', () => { it('getRequestHeaders should sign with scopes if user supplied scopes', () => { const client = new JWTAccess(email, keys.private); const headers = client.getRequestHeaders(testUri, undefined, 'myfakescope'); - const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + const decoded = jws.decode(removeBearerFromAuthorizationHeader(headers)); assert(decoded); const payload = decoded.payload; assert.strictEqual('myfakescope', payload.scope); @@ -69,7 +73,7 @@ describe('jwtaccess', () => { it('getRequestHeaders should sign with default if user did not supply scopes', () => { const client = new JWTAccess(email, keys.private); const headers = client.getRequestHeaders(testUri); - const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + const decoded = jws.decode(removeBearerFromAuthorizationHeader(headers)); assert(decoded); const payload = decoded.payload; assert.strictEqual(testUri, payload.aud); @@ -78,7 +82,7 @@ describe('jwtaccess', () => { it('getRequestHeaders should set key id in header when available', () => { const client = new JWTAccess(email, keys.private, '101'); const headers = client.getRequestHeaders(testUri); - const decoded = jws.decode(headers.Authorization.replace('Bearer ', '')); + const decoded = jws.decode(removeBearerFromAuthorizationHeader(headers)); assert(decoded); assert.deepStrictEqual(decoded.header, { alg: 'RS256', @@ -99,7 +103,7 @@ describe('jwtaccess', () => { const client = new JWTAccess(email, keys.private); const res = client.getRequestHeaders(testUri); const res2 = client.getRequestHeaders(testUri); - assert.strictEqual(res, res2); + assert.deepStrictEqual(res, res2); }); it('getRequestHeaders should not return cached tokens older than an hour', () => { diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 43c27c6d..9dc0a575 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -20,7 +20,6 @@ import * as fs from 'fs'; import {GaxiosError} from 'gaxios'; import * as nock from 'nock'; import * as path from 'path'; -import * as qs from 'querystring'; import * as sinon from 'sinon'; import { @@ -844,26 +843,22 @@ describe('oauth2', () => { }); }); - it('should be able to retrieve a list of Google certificates from cache again', done => { + it('should be able to retrieve a list of Google certificates from cache again', async () => { const scope = nock('https://www.googleapis.com') .defaultReplyHeaders({ 'Cache-Control': 'public, max-age=23641, must-revalidate, no-transform', - 'Content-Type': 'application/json', + 'content-type': 'application/json', }) .get(certsPath) .replyWithFile(200, certsResPath); - client.getFederatedSignonCerts((err, certs) => { - assert.strictEqual(err, null); - assert.strictEqual(Object.keys(certs!).length, 2); - scope.done(); // has retrieved from nock... nock no longer will reply - client.getFederatedSignonCerts((err2, certs2) => { - assert.strictEqual(err2, null); - assert.strictEqual(Object.keys(certs2!).length, 2); - scope.done(); - done(); - }); - }); + const {certs} = await client.getFederatedSignonCerts(); + assert.strictEqual(Object.keys(certs).length, 2); + scope.done(); // has retrieved from nock... nock no longer will reply + + const {certs: certs2} = await client.getFederatedSignonCerts(); + assert.strictEqual(Object.keys(certs2).length, 2); + scope.done(); // has retrieved from nock... nock no longer will reply }); it('should be able to retrieve a list of IAP certificates', done => { @@ -925,7 +920,9 @@ describe('oauth2', () => { function mockExample() { return [ nock(baseUrl, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, {access_token: 'abc123', expires_in: 1}), @@ -959,7 +956,9 @@ describe('oauth2', () => { // endpoint. This makes sure that refreshToken is called only once. const scopes = [ nock(baseUrl, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, {access_token: 'abc123', expires_in: 1}), @@ -981,7 +980,9 @@ describe('oauth2', () => { // the promise from getting cached for too long. const scopes = [ nock(baseUrl, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .twice() @@ -1001,7 +1002,9 @@ describe('oauth2', () => { // a second call to refreshToken, which should use a different promise. const scopes = [ nock(baseUrl, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(400) @@ -1029,7 +1032,9 @@ describe('oauth2', () => { const scopes = [ nock(baseUrl, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(400, reAuthErrorBody), @@ -1148,12 +1153,14 @@ describe('oauth2', () => { error: {code, message: 'Invalid Credentials'}, }), nock('http://example.com', { - reqheaders: {Authorization: 'Bearer abc123'}, + reqheaders: {authorization: 'Bearer abc123'}, }) .get('/access') .reply(200), nock(baseUrl, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, {access_token: 'abc123', expires_in: 1000}), @@ -1180,7 +1187,9 @@ describe('oauth2', () => { }); const scopes = [ nock(baseUrl, { - reqheaders: {'content-type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, {access_token: 'abc123', expires_in: 1000}), @@ -1190,7 +1199,7 @@ describe('oauth2', () => { error: {code, message: 'Invalid Credentials'}, }), nock('http://example.com', { - reqheaders: {Authorization: 'Bearer abc123'}, + reqheaders: {authorization: 'Bearer abc123'}, }) .get('/access') .reply(200), @@ -1211,7 +1220,7 @@ describe('oauth2', () => { it('should call refreshHandler in request() on token expiration and no refresh token available', async () => { const authHeaders = { - Authorization: 'Bearer access_token', + authorization: 'Bearer access_token', }; const scopes = [ nock('http://example.com') @@ -1253,7 +1262,7 @@ describe('oauth2', () => { it('should call refreshHandler in request() if no credentials available', async () => { const authHeaders = { - Authorization: 'Bearer access_token', + authorization: 'Bearer access_token', }; const scopes = [ nock('http://example.com') @@ -1336,7 +1345,9 @@ describe('oauth2', () => { it('getToken should allow a code_verifier to be passed', async () => { const scope = nock(baseUrl, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, { @@ -1350,14 +1361,16 @@ describe('oauth2', () => { }); scope.done(); assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.code_verifier, 'its_verified'); + + const params = new URLSearchParams(res.res.config.data || ''); + assert.strictEqual(params.get('code_verifier'), 'its_verified'); }); it('getToken should set redirect_uri if not provided in options', async () => { const scope = nock(baseUrl, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, { @@ -1368,14 +1381,16 @@ describe('oauth2', () => { const res = await client.getToken({code: 'code here'}); scope.done(); assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.redirect_uri, REDIRECT_URI); + + const params = new URLSearchParams(res.res.config.data || ''); + assert.strictEqual(params.get('redirect_uri'), REDIRECT_URI); }); it('getToken should set client_id if not provided in options', async () => { const scope = nock(baseUrl, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .matchHeader('authorization', value => value === undefined) @@ -1387,14 +1402,16 @@ describe('oauth2', () => { const res = await client.getToken({code: 'code here'}); scope.done(); assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.client_id, CLIENT_ID); + + const params = new URLSearchParams(res.res.config.data || ''); + assert.strictEqual(params.get('client_id'), CLIENT_ID); }); it('getToken should override redirect_uri if provided in options', async () => { const scope = nock(baseUrl, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, { @@ -1408,14 +1425,16 @@ describe('oauth2', () => { }); scope.done(); assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.redirect_uri, 'overridden'); + + const params = new URLSearchParams(res.res.config.data || ''); + assert.strictEqual(params.get('redirect_uri'), 'overridden'); }); it('getToken should override client_id if provided in options', async () => { const scope = nock(baseUrl, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, { @@ -1429,9 +1448,9 @@ describe('oauth2', () => { }); scope.done(); assert(res.res); - if (!res.res) return; - const params = qs.parse(res.res.config.data); - assert.strictEqual(params.client_id, 'overridden'); + + const params = new URLSearchParams(res.res.config.data || ''); + assert.strictEqual(params.get('client_id'), 'overridden'); }); it('getToken should use basic header auth if provided in options', async () => { @@ -1441,7 +1460,7 @@ describe('oauth2', () => { Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'); const scope = nock(authurl) .post('/oauthtoken') - .matchHeader('Authorization', basic_auth) + .matchHeader('authorization', basic_auth) .reply(200, { access_token: 'abc', refresh_token: '123', @@ -1472,7 +1491,7 @@ describe('oauth2', () => { const authurl = 'https://some.example.auth/'; const scope = nock(authurl) .post('/token') - .matchHeader('Authorization', val => val === undefined) + .matchHeader('authorization', val => val === undefined) .reply(401); const opts = { clientId: CLIENT_ID, @@ -1523,7 +1542,9 @@ describe('oauth2', () => { it('should return expiry_date', done => { const now = new Date().getTime(); const scope = nock(baseUrl, { - reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'}, + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, }) .post('/token') .reply(200, { @@ -1550,7 +1571,7 @@ describe('oauth2', () => { const scope = nock(baseUrl, { reqheaders: { - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', authorization: `Bearer ${accessToken}`, }, }) @@ -1572,9 +1593,9 @@ describe('oauth2', () => { client.refreshHandler = async () => { return expectedRefreshedAccessToken; }; - const expectedMetadata = { - Authorization: 'Bearer access_token', - }; + const expectedMetadata = new Headers({ + authorization: 'Bearer access_token', + }); assert.deepStrictEqual(client.credentials, {}); const requestMetaData = @@ -1595,9 +1616,9 @@ describe('oauth2', () => { access_token: 'initial-access-token', expiry_date: new Date().getTime() - 1000, }); - const expectedMetadata = { - Authorization: 'Bearer access_token', - }; + const expectedMetadata = new Headers({ + authorization: 'Bearer access_token', + }); const requestMetaData = await client.getRequestHeaders('http://example.com'); @@ -1610,9 +1631,9 @@ describe('oauth2', () => { access_token: 'initial-access-token', expiry_date: new Date().getTime() + 3600 * 1000, }; - const expectedMetadata = { - Authorization: 'Bearer initial-access-token', - }; + const expectedMetadata = new Headers({ + authorization: 'Bearer initial-access-token', + }); const requestMetaData = await client.getRequestHeaders('http://example.com'); @@ -1698,7 +1719,7 @@ describe('oauth2', () => { const scope = nock(url, { reqheaders: { - Authorization: `Bearer ${credentials.access_token}`, + authorization: `Bearer ${credentials.access_token}`, }, }) .get('/') diff --git a/test/test.oauth2common.ts b/test/test.oauth2common.ts index c0700190..17f7aaf2 100644 --- a/test/test.oauth2common.ts +++ b/test/test.oauth2common.ts @@ -17,7 +17,6 @@ import {describe, it} from 'mocha'; import * as assert from 'assert'; import * as querystring from 'querystring'; -import {Headers} from '../src/auth/authclient'; import { ClientAuthentication, OAuthClientAuthHandler, @@ -46,6 +45,13 @@ class CustomError extends Error { } } +function prepareExpectedOptions(options: GaxiosOptions) { + return { + ...options, + headers: new Headers(options.headers), + }; +} + describe('OAuthClientAuthHandler', () => { const basicAuth: ClientAuthentication = { confidentialClientType: 'basic', @@ -60,11 +66,11 @@ describe('OAuthClientAuthHandler', () => { }; // Base64 encoding of "username:" const expectedBase64EncodedCredNoSecret = 'dXNlcm5hbWU6'; - const reqBodyAuth: ClientAuthentication = { + const reqBodyAuth = { confidentialClientType: 'request-body', clientId: 'username', clientSecret: 'password', - }; + } as const; const reqBodyAuthNoSecret: ClientAuthentication = { confidentialClientType: 'request-body', clientId: 'username', @@ -72,83 +78,89 @@ describe('OAuthClientAuthHandler', () => { it('should not process request when no client authentication is used', () => { const handler = new TestOAuthClientAuthHandler(); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, data: { key1: 'value1', key2: 'value2', }, }; - const actualOptions = Object.assign({}, originalOptions); - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(originalOptions, actualOptions); + const expectedOptions = prepareExpectedOptions(options); + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); it('should process request with basic client auth', () => { const handler = new TestOAuthClientAuthHandler(basicAuth); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, data: { key1: 'value1', key2: 'value2', }, }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = - `Basic ${expectedBase64EncodedCred}`; - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.headers.set( + 'authorization', + `Basic ${expectedBase64EncodedCred}` + ); + + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); it('should process request with secretless basic client auth', () => { const handler = new TestOAuthClientAuthHandler(basicAuthNoSecret); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, data: { key1: 'value1', key2: 'value2', }, }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = - `Basic ${expectedBase64EncodedCredNoSecret}`; - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.headers.set( + 'authorization', + `Basic ${expectedBase64EncodedCredNoSecret}` + ); + + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); it('should process GET (non-request-body) with basic client auth', () => { const handler = new TestOAuthClientAuthHandler(basicAuth); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'GET', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = - `Basic ${expectedBase64EncodedCred}`; - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.headers.set( + 'authorization', + `Basic ${expectedBase64EncodedCred}` + ); + + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); describe('with request-body client auth', () => { @@ -187,7 +199,7 @@ describe('OAuthClientAuthHandler', () => { const handler = new TestOAuthClientAuthHandler(reqBodyAuth); const originalOptions: GaxiosOptions = { headers: { - 'Content-Type': 'text/html', + 'content-type': 'text/html', }, method: 'POST', url: 'https://www.example.com/path/to/api', @@ -200,205 +212,227 @@ describe('OAuthClientAuthHandler', () => { it('should inject creds in non-empty json content', () => { const handler = new TestOAuthClientAuthHandler(reqBodyAuth); - const originalOptions: GaxiosOptions = { + const options = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, data: { key1: 'value1', key2: 'value2', }, }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - expectedOptions.data.client_id = reqBodyAuth.clientId; - expectedOptions.data.client_secret = reqBodyAuth.clientSecret; - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.data = { + ...options.data, + client_id: reqBodyAuth.clientId, + client_secret: reqBodyAuth.clientSecret, + }; + + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); it('should inject secretless creds in json content', () => { const handler = new TestOAuthClientAuthHandler(reqBodyAuthNoSecret); - const originalOptions: GaxiosOptions = { + const options = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, data: { key1: 'value1', key2: 'value2', }, }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - expectedOptions.data.client_id = reqBodyAuthNoSecret.clientId; - expectedOptions.data.client_secret = ''; - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.data = { + ...options.data, + client_id: reqBodyAuthNoSecret.clientId, + client_secret: '', + }; + + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); it('should inject creds in empty json content', () => { const handler = new TestOAuthClientAuthHandler(reqBodyAuth); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); + + const expectedOptions = prepareExpectedOptions(options); expectedOptions.data = { client_id: reqBodyAuth.clientId, client_secret: reqBodyAuth.clientSecret, }; - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); it('should inject creds in non-empty x-www-form-urlencoded content', () => { const handler = new TestOAuthClientAuthHandler(reqBodyAuth); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', - headers: { - // Handling of headers should be case insensitive. - 'content-Type': 'application/x-www-form-urlencoded', - }, + headers: new Headers({ + 'content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }), data: querystring.stringify({key1: 'value1', key2: 'value2'}), }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - expectedOptions.data = querystring.stringify({ + + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.data = new URLSearchParams({ key1: 'value1', key2: 'value2', client_id: reqBodyAuth.clientId, client_secret: reqBodyAuth.clientSecret, }); - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); + }); + + it('should inject creds in non-empty URLSearchParams content', () => { + const handler = new TestOAuthClientAuthHandler(reqBodyAuth); + const options: GaxiosOptions = { + url: 'https://www.example.com/path/to/api', + method: 'POST', + data: new URLSearchParams({key1: 'value1', key2: 'value2'}), + }; + + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.data = new URLSearchParams({ + key1: 'value1', + key2: 'value2', + client_id: reqBodyAuth.clientId, + client_secret: reqBodyAuth.clientSecret, + }); + + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); it('should inject secretless creds in x-www-form-urlencoded content', () => { const handler = new TestOAuthClientAuthHandler(reqBodyAuthNoSecret); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, data: querystring.stringify({key1: 'value1', key2: 'value2'}), }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - expectedOptions.data = querystring.stringify({ + + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.data = new URLSearchParams({ key1: 'value1', key2: 'value2', client_id: reqBodyAuth.clientId, client_secret: '', }); - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); it('should inject creds in empty x-www-form-urlencoded content', () => { const handler = new TestOAuthClientAuthHandler(reqBodyAuth); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - expectedOptions.data = querystring.stringify({ + + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.data = new URLSearchParams({ client_id: reqBodyAuth.clientId, client_secret: reqBodyAuth.clientSecret, }); - handler.testApplyClientAuthenticationOptions(actualOptions); - assert.deepStrictEqual(expectedOptions, actualOptions); + handler.testApplyClientAuthenticationOptions(options); + assert.deepStrictEqual(options, expectedOptions); }); }); it('should process request with bearer token when provided', () => { const bearerToken = 'BEARER_TOKEN'; const handler = new TestOAuthClientAuthHandler(); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, data: { key1: 'value1', key2: 'value2', }, }; - const actualOptions = Object.assign({}, originalOptions); - const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = - `Bearer ${bearerToken}`; - handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); - assert.deepStrictEqual(expectedOptions, actualOptions); + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.headers.set('authorization', `Bearer ${bearerToken}`); + + handler.testApplyClientAuthenticationOptions(options, bearerToken); + + assert(options.headers instanceof Headers); + assert.deepStrictEqual(options, expectedOptions); }); it('should prioritize bearer token over basic auth', () => { const bearerToken = 'BEARER_TOKEN'; const handler = new TestOAuthClientAuthHandler(basicAuth); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, data: { key1: 'value1', key2: 'value2', }, }; - const actualOptions = Object.assign({}, originalOptions); - // Expected options should have bearer token in header. - const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = - `Bearer ${bearerToken}`; - - handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); - assert.deepStrictEqual(expectedOptions, actualOptions); + + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.headers.set('authorization', `Bearer ${bearerToken}`); + + handler.testApplyClientAuthenticationOptions(options, bearerToken); + assert.deepStrictEqual(options, expectedOptions); }); it('should prioritize bearer token over request body', () => { const bearerToken = 'BEARER_TOKEN'; const handler = new TestOAuthClientAuthHandler(reqBodyAuth); - const originalOptions: GaxiosOptions = { + const options: GaxiosOptions = { url: 'https://www.example.com/path/to/api', method: 'POST', headers: { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }, data: { key1: 'value1', key2: 'value2', }, }; - const actualOptions = Object.assign({}, originalOptions); - // Expected options should have bearer token in header. - const expectedOptions = Object.assign({}, originalOptions); - (expectedOptions.headers as Headers).Authorization = - `Bearer ${bearerToken}`; - - handler.testApplyClientAuthenticationOptions(actualOptions, bearerToken); - assert.deepStrictEqual(expectedOptions, actualOptions); + + const expectedOptions = prepareExpectedOptions(options); + expectedOptions.headers.set('authorization', `Bearer ${bearerToken}`); + + handler.testApplyClientAuthenticationOptions(options, bearerToken); + assert.deepStrictEqual(options, expectedOptions); }); }); diff --git a/test/test.passthroughclient.ts b/test/test.passthroughclient.ts index 19a15387..6e943eb9 100644 --- a/test/test.passthroughclient.ts +++ b/test/test.passthroughclient.ts @@ -37,11 +37,11 @@ describe('AuthClient', () => { }); describe('#getRequestHeaders', () => { - it('should return an empty object', async () => { + it('should return an empty `Headers` object', async () => { const client = new PassThroughClient(); const token = await client.getRequestHeaders(); - assert.deepEqual(token, {}); + assert.deepEqual(token, new Headers()); }); }); diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 4f7b888f..8522c3d4 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -149,7 +149,7 @@ describe('refresh', () => { await refresh.fromStream(stream); const headers = await refresh.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + assert.strictEqual(headers.get('x-goog-user-project'), 'my-quota-project'); req.done(); }); @@ -168,7 +168,7 @@ describe('refresh', () => { expiry_date: new Date().getTime() + eagerRefreshThresholdMillis + 1000, }; const headers = await refresh.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + assert.strictEqual(headers.get('x-goog-user-project'), 'my-quota-project'); }); it('getRequestHeaders should populate x-goog-user-project header if quota_project_id present and token has expired', async () => { @@ -186,7 +186,7 @@ describe('refresh', () => { expiry_date: new Date().getTime() - 1, }; const headers = await refresh.getRequestHeaders(); - assert.strictEqual(headers['x-goog-user-project'], 'my-quota-project'); + assert.strictEqual(headers.get('x-goog-user-project'), 'my-quota-project'); req.done(); }); }); diff --git a/test/test.stscredentials.ts b/test/test.stscredentials.ts index 4aafd20d..b183e8af 100644 --- a/test/test.stscredentials.ts +++ b/test/test.stscredentials.ts @@ -14,7 +14,6 @@ import * as assert from 'assert'; import {describe, it, afterEach} from 'mocha'; -import * as qs from 'querystring'; import * as nock from 'nock'; import {createCrypto} from '../src/crypto/crypto'; import { @@ -96,12 +95,12 @@ describe('StsCredentials', () => { ): nock.Scope { const headers = Object.assign( { - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, additionalHeaders || {} ); return nock(baseUrl) - .post(path, qs.stringify(request), { + .post(path, request, { reqheaders: headers, }) .reply(statusCode, response); @@ -218,17 +217,27 @@ describe('StsCredentials', () => { it('should handle and retry on timeout', async () => { const scope = nock(baseUrl) - .post(path, qs.stringify(expectedRequest), { - reqheaders: { - 'content-type': 'application/x-www-form-urlencoded', - }, - }) - .replyWithError({code: 'ETIMEDOUT'}) - .post(path, qs.stringify(expectedRequest), { - reqheaders: { - 'content-type': 'application/x-www-form-urlencoded', - }, - }) + .post( + path, + {...expectedRequest}, + { + reqheaders: { + 'content-type': + 'application/x-www-form-urlencoded;charset=UTF-8', + }, + } + ) + .replyWithError('ETIMEDOUT') + .post( + path, + {...expectedRequest}, + { + reqheaders: { + 'content-type': + 'application/x-www-form-urlencoded;charset=UTF-8', + }, + } + ) .reply(200, stsSuccessfulResponse); const stsCredentials = new StsCredentials(tokenExchangeEndpoint); @@ -254,7 +263,7 @@ describe('StsCredentials', () => { expectedRequest, Object.assign( { - Authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, + authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, }, additionalHeaders ) @@ -283,7 +292,7 @@ describe('StsCredentials', () => { stsSuccessfulResponse, expectedPartialRequest, { - Authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, + authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, } ); const stsCredentials = new StsCredentials( @@ -310,7 +319,7 @@ describe('StsCredentials', () => { expectedRequest, Object.assign( { - Authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, + authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, }, additionalHeaders ) From a511e562783402468093d70e77e73762fa95ae79 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 21 Feb 2025 10:29:01 +0100 Subject: [PATCH 590/662] chore(deps): update dependency gts to v6 (#1886) * chore(deps): update dependency gts to v6 * style: lint fix * chore: update config * chore: lint * chore: clean-up * style: lint * chore: clean-up internal --------- Co-authored-by: Daniel Bankhead --- browser-test/test.crypto.ts | 10 +- browser-test/test.oauth2.ts | 16 +- package.json | 14 +- samples/authenticateAPIKey.js | 2 +- samples/idtokens-iap.js | 2 +- samples/idtokens-serverless.js | 2 +- samples/jwt.js | 2 +- samples/keyfile.js | 2 +- samples/oauth2.js | 2 +- samples/scripts/downscoping-with-cab-setup.js | 2 +- samples/scripts/externalclient-setup.js | 6 +- samples/test/auth.test.js | 8 +- samples/test/externalclient.test.js | 12 +- samples/verifyIdToken-iap.js | 6 +- src/auth/authclient.ts | 4 +- src/auth/awsclient.ts | 26 +- src/auth/awsrequestsigner.ts | 20 +- src/auth/baseexternalclient.ts | 37 ++- src/auth/computeclient.ts | 5 +- .../defaultawssecuritycredentialssupplier.ts | 18 +- src/auth/downscopedclient.ts | 16 +- src/auth/executable-response.ts | 14 +- .../externalAccountAuthorizedUserClient.ts | 12 +- src/auth/externalclient.ts | 4 +- src/auth/filesubjecttokensupplier.ts | 7 +- src/auth/googleauth.ts | 69 +++-- src/auth/iam.ts | 2 +- src/auth/identitypoolclient.ts | 16 +- src/auth/idtokenclient.ts | 4 +- src/auth/impersonated.ts | 4 +- src/auth/jwtaccess.ts | 20 +- src/auth/jwtclient.ts | 29 +-- src/auth/oauth2client.ts | 101 ++++---- src/auth/oauth2common.ts | 17 +- src/auth/passthrough.ts | 4 - src/auth/pluggable-auth-client.ts | 12 +- src/auth/pluggable-auth-handler.ts | 18 +- src/auth/refreshclient.ts | 24 +- src/auth/stscredentials.ts | 6 +- src/auth/urlsubjecttokensupplier.ts | 4 +- src/crypto/browser/crypto.ts | 20 +- src/crypto/node/crypto.ts | 8 +- src/crypto/shared.ts | 6 +- src/util.ts | 11 +- system-test/fixtures/kitchen/package.json | 2 +- system-test/test.kitchen.ts | 6 +- test/externalclienthelper.ts | 22 +- test/test.authclient.ts | 6 +- test/test.awsclient.ts | 93 ++++--- test/test.awsrequestsigner.ts | 12 +- test/test.baseexternalclient.ts | 194 +++++++------- test/test.compute.ts | 16 +- test/test.crypto.ts | 10 +- test/test.downscopedclient.ts | 96 +++---- test/test.executableresponse.ts | 35 +-- ...est.externalaccountauthorizeduserclient.ts | 64 ++--- test/test.externalclient.ts | 28 +- test/test.googleauth.ts | 244 +++++++++--------- test/test.identitypoolclient.ts | 121 +++++---- test/test.idtokenclient.ts | 4 +- test/test.impersonated.ts | 36 +-- test/test.jwt.ts | 41 +-- test/test.jwtaccess.ts | 2 +- test/test.oauth2.ts | 110 ++++---- test/test.oauth2common.ts | 20 +- test/test.pluggableauthclient.ts | 49 ++-- test/test.pluggableauthhandler.ts | 62 ++--- test/test.refresh.ts | 8 +- test/test.stscredentials.ts | 70 ++--- tsconfig.json | 12 +- 70 files changed, 970 insertions(+), 1017 deletions(-) diff --git a/browser-test/test.crypto.ts b/browser-test/test.crypto.ts index 37e2d5f2..bd6f1ab2 100644 --- a/browser-test/test.crypto.ts +++ b/browser-test/test.crypto.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as base64js from 'base64-js'; -import {assert} from 'chai'; +import {strict as assert} from 'assert'; import {createCrypto, fromArrayBufferToHex} from '../src/crypto/crypto'; import {BrowserCrypto} from '../src/crypto/browser/crypto'; import {privateKey, publicKey} from './fixtures/keys'; @@ -112,8 +112,8 @@ describe('Browser crypto tests', () => { 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'; const expectedHash = new Uint8Array( (expectedHexHash.match(/.{1,2}/g) as string[]).map(byte => - parseInt(byte, 16) - ) + parseInt(byte, 16), + ), ); const calculatedHash = await crypto.signWithHmacSha256(key, message); @@ -129,8 +129,8 @@ describe('Browser crypto tests', () => { 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'; const expectedHash = new Uint8Array( (expectedHexHash.match(/.{1,2}/g) as string[]).map(byte => - parseInt(byte, 16) - ) + parseInt(byte, 16), + ), ); const calculatedHash = await crypto.signWithHmacSha256(key, message); diff --git a/browser-test/test.oauth2.ts b/browser-test/test.oauth2.ts index 80e1bbea..1ab37e64 100644 --- a/browser-test/test.oauth2.ts +++ b/browser-test/test.oauth2.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as base64js from 'base64-js'; -import {assert} from 'chai'; +import {strict as assert} from 'assert'; import * as sinon from 'sinon'; import {privateKey, publicKey} from './fixtures/keys'; import {it, describe, beforeEach} from 'mocha'; @@ -89,8 +89,8 @@ describe('Browser OAuth2 tests', () => { client.transporter.request = stub; const response = await client.getToken('code here'); const tokens = response.tokens; - assert.isAbove(tokens!.expiry_date!, now + 10 * 1000); - assert.isBelow(tokens!.expiry_date!, now + 15 * 1000); + assert(tokens!.expiry_date! > now + 10 * 1000); + assert(tokens!.expiry_date! < now + 15 * 1000); }); it('getFederatedSignonCerts talks to correct endpoint', async () => { @@ -135,7 +135,7 @@ describe('Browser OAuth2 tests', () => { assert.strictEqual(params.get('code_challenge'), codes.codeChallenge); assert.strictEqual( params.get('code_challenge_method'), - CodeChallengeMethod.S256 + CodeChallengeMethod.S256, ); }); @@ -167,25 +167,23 @@ describe('Browser OAuth2 tests', () => { name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'}, }; - // eslint-disable-next-line no-undef const cryptoKey = await window.crypto.subtle.importKey( 'jwk', privateKey, algo, true, - ['sign'] + ['sign'], ); - // eslint-disable-next-line no-undef const signature = await window.crypto.subtle.sign( algo, cryptoKey, - new TextEncoder().encode(data) + new TextEncoder().encode(data), ); data += '.' + base64js.fromByteArray(new Uint8Array(signature)); const login = await client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ); assert.strictEqual(login.getUserId(), '123456789'); }); diff --git a/package.json b/package.json index cfa97071..2fc27050 100644 --- a/package.json +++ b/package.json @@ -26,19 +26,17 @@ }, "devDependencies": { "@types/base64-js": "^1.2.5", - "@types/chai": "^4.1.7", "@types/jws": "^3.1.0", - "@types/mocha": "^9.0.0", + "@types/mocha": "^10.0.10", "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", "@types/node": "^22.0.0", "@types/sinon": "^17.0.0", "assert-rejects": "^1.0.0", "c8": "^10.0.0", - "chai": "^4.2.0", "codecov": "^3.0.2", - "gts": "^5.0.0", - "is-docker": "^2.0.0", + "gts": "^6.0.0", + "is-docker": "^3.0.0", "jsdoc": "^4.0.0", "jsdoc-fresh": "^3.0.0", "jsdoc-region-tag": "^3.0.0", @@ -48,16 +46,16 @@ "karma-firefox-launcher": "^2.0.0", "karma-mocha": "^2.0.0", "karma-sourcemap-loader": "^0.4.0", - "karma-webpack": "5.0.0", + "karma-webpack": "^5.0.1", "keypair": "^1.0.4", "linkinator": "^6.1.2", - "mocha": "^9.2.2", + "mocha": "^11.1.0", "mv": "^2.1.1", "ncp": "^2.0.0", "nock": "^14.0.1", "null-loader": "^4.0.0", "puppeteer": "^24.0.0", - "sinon": "^18.0.0", + "sinon": "^18.0.1", "ts-loader": "^8.0.0", "typescript": "^5.1.6", "webpack": "^5.21.2", diff --git a/samples/authenticateAPIKey.js b/samples/authenticateAPIKey.js index b42ba031..635376f1 100644 --- a/samples/authenticateAPIKey.js +++ b/samples/authenticateAPIKey.js @@ -43,7 +43,7 @@ function main() { console.log(`Text: ${text}`); console.log( - `Sentiment: ${response.documentSentiment.score}, ${response.documentSentiment.magnitude}` + `Sentiment: ${response.documentSentiment.score}, ${response.documentSentiment.magnitude}`, ); console.log('Successfully authenticated using the API key'); } diff --git a/samples/idtokens-iap.js b/samples/idtokens-iap.js index d2142dae..1575ed82 100644 --- a/samples/idtokens-iap.js +++ b/samples/idtokens-iap.js @@ -20,7 +20,7 @@ function main( url = 'https://some.iap.url', - targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com' + targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com', ) { // [START iap_make_request] /** diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index 5bbba470..e48a9ae1 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -20,7 +20,7 @@ function main( url = 'https://service-1234-uc.a.run.app', - targetAudience = null + targetAudience = null, ) { if (!targetAudience) { // Use the target service's hostname as the target audience for requests. diff --git a/samples/jwt.js b/samples/jwt.js index 62b9cccd..8bf91998 100644 --- a/samples/jwt.js +++ b/samples/jwt.js @@ -29,7 +29,7 @@ const fs = require('fs'); async function main( // Full path to the service account credential - keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS + keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS, ) { const keys = JSON.parse(fs.readFileSync(keyFile, 'utf8')); const client = new JWT({ diff --git a/samples/keyfile.js b/samples/keyfile.js index 3018d843..4a977d38 100644 --- a/samples/keyfile.js +++ b/samples/keyfile.js @@ -24,7 +24,7 @@ const {GoogleAuth} = require('google-auth-library'); */ async function main( // Full path to the service account credential - keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS + keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS, ) { const auth = new GoogleAuth({ keyFile: keyFile, diff --git a/samples/oauth2.js b/samples/oauth2.js index cabf1e2d..9c441394 100644 --- a/samples/oauth2.js +++ b/samples/oauth2.js @@ -40,7 +40,7 @@ async function main() { // After acquiring an access_token, you may want to check on the audience, expiration, // or original scopes requested. You can do that with the `getTokenInfo` method. const tokenInfo = await oAuth2Client.getTokenInfo( - oAuth2Client.credentials.access_token + oAuth2Client.credentials.access_token, ); console.log(tokenInfo); } diff --git a/samples/scripts/downscoping-with-cab-setup.js b/samples/scripts/downscoping-with-cab-setup.js index a0f37700..0ec78b1f 100644 --- a/samples/scripts/downscoping-with-cab-setup.js +++ b/samples/scripts/downscoping-with-cab-setup.js @@ -57,7 +57,7 @@ function generateRandomString(length) { const allowedChars = 'abcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i++) { chars.push( - allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)) + allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)), ); } return chars.join(''); diff --git a/samples/scripts/externalclient-setup.js b/samples/scripts/externalclient-setup.js index caf24bd7..f1a894e0 100755 --- a/samples/scripts/externalclient-setup.js +++ b/samples/scripts/externalclient-setup.js @@ -101,7 +101,7 @@ function generateRandomString(length) { const allowedChars = 'abcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i++) { chars.push( - allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)) + allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)), ); } return chars.join(''); @@ -276,12 +276,12 @@ const config = { main(config) .then(audiences => { console.log( - 'The following constants need to be set in test/externalclient.test.js' + 'The following constants need to be set in test/externalclient.test.js', ); console.log(`AUDIENCE_OIDC='${audiences.oidcAudience}'`); console.log(`AUDIENCE_AWS='${audiences.awsAudience}'`); console.log( - `AWS_ROLE_ARN='arn:aws:iam::${config.awsAccountId}:role/${config.awsRoleName}'` + `AWS_ROLE_ARN='arn:aws:iam::${config.awsAccountId}:role/${config.awsRoleName}'`, ); }) .catch(console.error); diff --git a/samples/test/auth.test.js b/samples/test/auth.test.js index 7473fc4d..249e52f5 100644 --- a/samples/test/auth.test.js +++ b/samples/test/auth.test.js @@ -40,7 +40,7 @@ describe('auth samples', () => { const projectId = await auth.getProjectId(); const output = execSync( - `node authenticateImplicitWithAdc ${projectId} ${ZONE}` + `node authenticateImplicitWithAdc ${projectId} ${ZONE}`, ); assert.match(output, /Listed all storage buckets./); @@ -48,7 +48,7 @@ describe('auth samples', () => { it('should get id token from metadata server', async () => { const output = execSync( - 'node idTokenFromMetadataServer https://www.google.com' + 'node idTokenFromMetadataServer https://www.google.com', ); assert.match(output, /Generated ID token./); @@ -56,7 +56,7 @@ describe('auth samples', () => { it('should get id token from service account', async () => { const output = execSync( - `node idTokenFromServiceAccount ${TARGET_AUDIENCE} ${keyFile}` + `node idTokenFromServiceAccount ${TARGET_AUDIENCE} ${keyFile}`, ); assert.match(output, /Generated ID token./); @@ -69,7 +69,7 @@ describe('auth samples', () => { const idToken = await client.fetchIdToken(TARGET_AUDIENCE); const output = execSync( - `node verifyGoogleIdToken ${idToken} ${TARGET_AUDIENCE} https://www.googleapis.com/oauth2/v3/certs` + `node verifyGoogleIdToken ${idToken} ${TARGET_AUDIENCE} https://www.googleapis.com/oauth2/v3/certs`, ); assert.match(output, /ID token verified./); diff --git a/samples/test/externalclient.test.js b/samples/test/externalclient.test.js index 67631c62..f56bca7b 100644 --- a/samples/test/externalclient.test.js +++ b/samples/test/externalclient.test.js @@ -146,7 +146,7 @@ const assumeRoleWithWebIdentity = async ( auth, aud, clientEmail, - awsRoleArn + awsRoleArn, ) => { // API documented at: // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html @@ -186,7 +186,7 @@ const generateRandomString = length => { const allowedChars = 'abcdefghijklmnopqrstuvwxyz0123456789'; while (length > 0) { chars.push( - allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)) + allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)), ); length--; } @@ -242,7 +242,7 @@ describe('samples for external-account', () => { auth, clientId, clientEmail, - AWS_ROLE_ARN + AWS_ROLE_ARN, ); }); @@ -361,7 +361,7 @@ describe('samples for external-account', () => { res.end( JSON.stringify({ access_token: oidcToken, - }) + }), ); } else { res.setHeader('content-type', 'application/json'); @@ -369,7 +369,7 @@ describe('samples for external-account', () => { res.end( JSON.stringify({ error: 'missing-header', - }) + }), ); } } else { @@ -506,7 +506,7 @@ describe('samples for external-account', () => { const actualExpireTime = new Date(token.res.data.expireTime).getTime(); assert.isTrue( - minExpireTime <= actualExpireTime && actualExpireTime <= maxExpireTime + minExpireTime <= actualExpireTime && actualExpireTime <= maxExpireTime, ); }); }); diff --git a/samples/verifyIdToken-iap.js b/samples/verifyIdToken-iap.js index 70f3c6df..ca15fac2 100644 --- a/samples/verifyIdToken-iap.js +++ b/samples/verifyIdToken-iap.js @@ -28,7 +28,7 @@ function main( iapJwt, projectNumber = '', projectId = '', - backendServiceId = '' + backendServiceId = '', ) { // [START iap_validate_jwt] /** @@ -54,7 +54,7 @@ function main( iapJwt, response.pubkeys, expectedAudience, - ['https://cloud.google.com/iap'] + ['https://cloud.google.com/iap'], ); // Print out the info contained in the IAP ID token console.log(ticket); @@ -65,7 +65,7 @@ function main( // [END iap_validate_jwt] if (!expectedAudience) { console.log( - 'Audience not verified! Supply a projectNumber and projectID to verify' + 'Audience not verified! Supply a projectNumber and projectID to verify', ); } } diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 09f09ba4..3c8f12b1 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -227,7 +227,7 @@ export abstract class AuthClient if (options.get('useAuthRequestParameters') !== false) { this.transporter.interceptors.request.add( - AuthClient.DEFAULT_REQUEST_INTERCEPTOR + AuthClient.DEFAULT_REQUEST_INTERCEPTOR, ); } @@ -306,7 +306,7 @@ export abstract class AuthClient */ protected addUserProjectAndAuthHeaders( target: T, - source: Headers + source: Headers, ): T { const xGoogUserProject = source.get('x-goog-user-project'); const authorizationHeader = source.get('authorization'); diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index 504511c6..297c3794 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -97,7 +97,7 @@ export interface AwsSecurityCredentialsSupplier { * @return A promise that resolves with the requested {@link AwsSecurityCredentials}. */ getAwsSecurityCredentials: ( - context: ExternalAccountSupplierContext + context: ExternalAccountSupplierContext, ) => Promise; } @@ -133,23 +133,23 @@ export class AwsClient extends BaseExternalAccountClient { * from the external account JSON credential file. */ constructor( - options: AwsClientOptions | SnakeToCamelObject + options: AwsClientOptions | SnakeToCamelObject, ) { super(options); const opts = originalOrCamelOptions(options as AwsClientOptions); const credentialSource = opts.get('credential_source'); const awsSecurityCredentialsSupplier = opts.get( - 'aws_security_credentials_supplier' + 'aws_security_credentials_supplier', ); // Validate credential sourcing configuration. if (!credentialSource && !awsSecurityCredentialsSupplier) { throw new Error( - 'A credential source or AWS security credentials supplier must be specified.' + 'A credential source or AWS security credentials supplier must be specified.', ); } if (credentialSource && awsSecurityCredentialsSupplier) { throw new Error( - 'Only one of credential source or AWS security credentials supplier can be specified.' + 'Only one of credential source or AWS security credentials supplier can be specified.', ); } @@ -168,7 +168,7 @@ export class AwsClient extends BaseExternalAccountClient { // environment variables. const securityCredentialsUrl = credentialSourceOpts.get('url'); const imdsV2SessionTokenUrl = credentialSourceOpts.get( - 'imdsv2_session_token_url' + 'imdsv2_session_token_url', ); this.awsSecurityCredentialsSupplier = new DefaultAwsSecurityCredentialsSupplier({ @@ -178,7 +178,7 @@ export class AwsClient extends BaseExternalAccountClient { }); this.regionalCredVerificationUrl = credentialSourceOpts.get( - 'regional_cred_verification_url' + 'regional_cred_verification_url', ); this.credentialSourceType = 'aws'; @@ -195,7 +195,7 @@ export class AwsClient extends BaseExternalAccountClient { throw new Error('No valid AWS "credential_source" provided'); } else if (parseInt(match[2], 10) !== 1) { throw new Error( - `aws version "${match[2]}" is not supported in the current build.` + `aws version "${match[2]}" is not supported in the current build.`, ); } } @@ -212,11 +212,11 @@ export class AwsClient extends BaseExternalAccountClient { // Initialize AWS request signer if not already initialized. if (!this.awsRequestSigner) { this.region = await this.awsSecurityCredentialsSupplier.getAwsRegion( - this.supplierContext + this.supplierContext, ); this.awsRequestSigner = new AwsRequestSigner(async () => { return this.awsSecurityCredentialsSupplier.getAwsSecurityCredentials( - this.supplierContext + this.supplierContext, ); }, this.region); } @@ -249,12 +249,12 @@ export class AwsClient extends BaseExternalAccountClient { // ensure data integrity. 'x-goog-cloud-target-resource': this.audience, }, - options.headers + options.headers, ); // Reformat header to GCP STS expected format. extendedHeaders.forEach((value, key) => - reformattedHeader.push({key, value}) + reformattedHeader.push({key, value}), ); // Serialize the reformatted signed request. @@ -263,7 +263,7 @@ export class AwsClient extends BaseExternalAccountClient { url: options.url, method: options.method, headers: reformattedHeader, - }) + }), ); } } diff --git a/src/auth/awsrequestsigner.ts b/src/auth/awsrequestsigner.ts index 8b95d6e3..7807ff70 100644 --- a/src/auth/awsrequestsigner.ts +++ b/src/auth/awsrequestsigner.ts @@ -87,7 +87,7 @@ export class AwsRequestSigner { */ constructor( private readonly getCredentials: () => Promise, - private readonly region: string + private readonly region: string, ) { this.crypto = createCrypto(); } @@ -119,7 +119,7 @@ export class AwsRequestSigner { if (typeof requestPayload !== 'string' && requestPayload !== undefined) { throw new TypeError( - `'requestPayload' is expected to be a string if provided. Got: ${requestPayload}` + `'requestPayload' is expected to be a string if provided. Got: ${requestPayload}`, ); } @@ -142,7 +142,7 @@ export class AwsRequestSigner { authorization: headerMap.authorizationHeader, host: uri.host, }, - additionalAmzHeaders || {} + additionalAmzHeaders || {}, ); if (awsSecurityCredentials.token) { Gaxios.mergeHeaders(headers, { @@ -176,7 +176,7 @@ export class AwsRequestSigner { async function sign( crypto: Crypto, key: string | ArrayBuffer, - msg: string + msg: string, ): Promise { return await crypto.signWithHmacSha256(key, msg); } @@ -199,7 +199,7 @@ async function getSigningKey( key: string, dateStamp: string, region: string, - serviceName: string + serviceName: string, ): Promise { const kDate = await sign(crypto, `AWS4${key}`, dateStamp); const kRegion = await sign(crypto, kDate, region); @@ -217,10 +217,10 @@ async function getSigningKey( * components: amz-date, authorization header and canonical query string. */ async function generateAuthenticationHeaderMap( - options: GenerateAuthHeaderMapOptions + options: GenerateAuthHeaderMapOptions, ): Promise { const additionalAmzHeaders = Gaxios.mergeHeaders( - options.additionalAmzHeaders + options.additionalAmzHeaders, ); const requestPayload = options.requestPayload || ''; // iam.amazonaws.com host => iam service. @@ -239,7 +239,7 @@ async function generateAuthenticationHeaderMap( if (options.securityCredentials.token) { additionalAmzHeaders.set( 'x-amz-security-token', - options.securityCredentials.token + options.securityCredentials.token, ); } // Header keys need to be sorted alphabetically. @@ -250,7 +250,7 @@ async function generateAuthenticationHeaderMap( // Previously the date was not fixed with x-amz- and could be provided manually. // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req additionalAmzHeaders.has('date') ? {} : {'x-amz-date': amzDate}, - additionalAmzHeaders + additionalAmzHeaders, ); let canonicalHeaders = ''; @@ -285,7 +285,7 @@ async function generateAuthenticationHeaderMap( options.securityCredentials.secretAccessKey, dateStamp, options.region, - serviceName + serviceName, ); const signature = await sign(options.crypto, signingKey, stringToSign); // https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 9a39fdf0..2c337583 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -31,6 +31,7 @@ import { import * as sts from './stscredentials'; import {ClientAuthentication} from './oauth2common'; import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; +import {pkg} from '../shared.cjs'; /** * The required token exchange grant_type: rfc8693#section-2.1 @@ -69,9 +70,6 @@ const WORKFORCE_AUDIENCE_PATTERN = '//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; const DEFAULT_TOKEN_URL = 'https://sts.{universeDomain}/v1/token'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const pkg = require('../../../package.json'); - /** * Shared options used to build {@link ExternalAccountClient} and * {@link ExternalAccountAuthorizedUserClient}. @@ -205,8 +203,7 @@ export interface ProjectInfo { lifecycleState: string; name: string; createTime?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parent: {[key: string]: any}; + parent: {[key: string]: ReturnType}; } /** @@ -266,19 +263,19 @@ export abstract class BaseExternalAccountClient extends AuthClient { constructor( options: | BaseExternalAccountClientOptions - | SnakeToCamelObject + | SnakeToCamelObject, ) { super(options); const opts = originalOrCamelOptions( - options as BaseExternalAccountClientOptions + options as BaseExternalAccountClientOptions, ); const type = opts.get('type'); if (type && type !== EXTERNAL_ACCOUNT_TYPE) { throw new Error( `Expected "${EXTERNAL_ACCOUNT_TYPE}" type but ` + - `received "${options.type}"` + `received "${options.type}"`, ); } @@ -290,18 +287,18 @@ export abstract class BaseExternalAccountClient extends AuthClient { const subjectTokenType = opts.get('subject_token_type'); const workforcePoolUserProject = opts.get('workforce_pool_user_project'); const serviceAccountImpersonationUrl = opts.get( - 'service_account_impersonation_url' + 'service_account_impersonation_url', ); const serviceAccountImpersonation = opts.get( - 'service_account_impersonation' + 'service_account_impersonation', ); const serviceAccountImpersonationLifetime = originalOrCamelOptions( - serviceAccountImpersonation + serviceAccountImpersonation, ).get('token_lifetime_seconds'); this.cloudResourceManagerURL = new URL( opts.get('cloud_resource_manager_url') || - `https://cloudresourcemanager.${this.universeDomain}/v1/projects/` + `https://cloudresourcemanager.${this.universeDomain}/v1/projects/`, ); if (clientId) { @@ -328,7 +325,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { ) { throw new Error( 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' + 'credentials.', ); } @@ -360,7 +357,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { * @see {@link https://github.com/googleapis/google-auth-library-nodejs/security/code-scanning/84} **/ throw new RangeError( - `URL is too long: ${this.serviceAccountImpersonationUrl}` + `URL is too long: ${this.serviceAccountImpersonationUrl}`, ); } @@ -438,14 +435,14 @@ export abstract class BaseExternalAccountClient extends AuthClient { request(opts: GaxiosOptions, callback: BodyResponseCallback): void; request( opts: GaxiosOptions, - callback?: BodyResponseCallback + callback?: BodyResponseCallback, ): GaxiosPromise | void { if (callback) { this.requestAsync(opts).then( r => callback(null, r), e => { return callback(e, e.response); - } + }, ); } else { return this.requestAsync(opts); @@ -495,7 +492,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { */ protected async requestAsync( opts: GaxiosOptions, - reAuthRetried = false + reAuthRetried = false, ): Promise> { let response: GaxiosResponse; try { @@ -588,12 +585,12 @@ export abstract class BaseExternalAccountClient extends AuthClient { const stsResponse = await this.stsCredential.exchangeToken( stsCredentialsOptions, additionalHeaders, - additionalOptions + additionalOptions, ); if (this.serviceAccountImpersonationUrl) { this.cachedAccessToken = await this.getImpersonatedAccessToken( - stsResponse.access_token + stsResponse.access_token, ); } else if (stsResponse.expires_in) { // Save response in cached access token. @@ -655,7 +652,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { * credentials response. */ private async getImpersonatedAccessToken( - token: string + token: string, ): Promise { const opts: GaxiosOptions = { ...BaseExternalAccountClient.RETRY_CONFIG, diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index fbcec074..36ca73d1 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -63,10 +63,7 @@ export class Compute extends OAuth2Client { * Refreshes the access token. * @param refreshToken Unused parameter */ - protected async refreshTokenNoCache( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - refreshToken?: string | null - ): Promise { + protected async refreshTokenNoCache(): Promise { const tokenPath = `service-accounts/${this.serviceAccountEmail}/token`; let data: CredentialRequest; try { diff --git a/src/auth/defaultawssecuritycredentialssupplier.ts b/src/auth/defaultawssecuritycredentialssupplier.ts index e7a4025e..e215d1f0 100644 --- a/src/auth/defaultawssecuritycredentialssupplier.ts +++ b/src/auth/defaultawssecuritycredentialssupplier.ts @@ -113,13 +113,13 @@ export class DefaultAwsSecurityCredentialsSupplier if (!this.#regionFromEnv && this.imdsV2SessionTokenUrl) { metadataHeaders.set( 'x-aws-ec2-metadata-token', - await this.#getImdsV2SessionToken(context.transporter) + await this.#getImdsV2SessionToken(context.transporter), ); } if (!this.regionUrl) { throw new RangeError( 'Unable to determine AWS region due to missing ' + - '"options.credential_source.region_url"' + '"options.credential_source.region_url"', ); } const opts: GaxiosOptions = { @@ -144,7 +144,7 @@ export class DefaultAwsSecurityCredentialsSupplier * @return A promise that resolves with the AWS security credentials. */ async getAwsSecurityCredentials( - context: ExternalAccountSupplierContext + context: ExternalAccountSupplierContext, ): Promise { // Check environment variables for permanent credentials first. // https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html @@ -156,13 +156,13 @@ export class DefaultAwsSecurityCredentialsSupplier if (this.imdsV2SessionTokenUrl) { metadataHeaders.set( 'x-aws-ec2-metadata-token', - await this.#getImdsV2SessionToken(context.transporter) + await this.#getImdsV2SessionToken(context.transporter), ); } // Since the role on a VM can change, we don't need to cache it. const roleName = await this.#getAwsRoleName( metadataHeaders, - context.transporter + context.transporter, ); // Temporary credentials typically last for several hours. // Expiration is returned in response. @@ -171,7 +171,7 @@ export class DefaultAwsSecurityCredentialsSupplier const awsCreds = await this.#retrieveAwsSecurityCredentials( roleName, metadataHeaders, - context.transporter + context.transporter, ); return { accessKeyId: awsCreds.AccessKeyId, @@ -203,12 +203,12 @@ export class DefaultAwsSecurityCredentialsSupplier */ async #getAwsRoleName( headers: Headers, - transporter: Gaxios + transporter: Gaxios, ): Promise { if (!this.securityCredentialsUrl) { throw new Error( 'Unable to determine AWS role name due to missing ' + - '"options.credential_source.url"' + '"options.credential_source.url"', ); } const opts: GaxiosOptions = { @@ -233,7 +233,7 @@ export class DefaultAwsSecurityCredentialsSupplier async #retrieveAwsSecurityCredentials( roleName: string, headers: Headers, - transporter: Gaxios + transporter: Gaxios, ): Promise { const response = await transporter.request({ ...this.additionalGaxiosOptions, diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index bcc207b0..bc0d19b1 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -149,7 +149,7 @@ export class DownscopedClient extends AuthClient { accessBoundary: { accessBoundaryRules: [], }, - } + }, ) { super(options instanceof AuthClient ? {} : options); @@ -174,7 +174,7 @@ export class DownscopedClient extends AuthClient { ) { throw new Error( 'The provided access boundary has more than ' + - `${MAX_ACCESS_BOUNDARY_RULES_COUNT} access boundary rules.` + `${MAX_ACCESS_BOUNDARY_RULES_COUNT} access boundary rules.`, ); } @@ -184,7 +184,7 @@ export class DownscopedClient extends AuthClient { .accessBoundaryRules) { if (rule.availablePermissions.length === 0) { throw new Error( - 'At least one permission should be defined in access boundary rules.' + 'At least one permission should be defined in access boundary rules.', ); } } @@ -206,7 +206,7 @@ export class DownscopedClient extends AuthClient { if (!credentials.expiry_date) { throw new Error( 'The access token expiry_date field is missing in the provided ' + - 'credentials.' + 'credentials.', ); } super.setCredentials(credentials); @@ -260,14 +260,14 @@ export class DownscopedClient extends AuthClient { request(opts: GaxiosOptions, callback: BodyResponseCallback): void; request( opts: GaxiosOptions, - callback?: BodyResponseCallback + callback?: BodyResponseCallback, ): GaxiosPromise | void { if (callback) { this.requestAsync(opts).then( r => callback(null, r), e => { return callback(e, e.response); - } + }, ); } else { return this.requestAsync(opts); @@ -283,7 +283,7 @@ export class DownscopedClient extends AuthClient { */ protected async requestAsync( opts: GaxiosOptions, - reAuthRetried = false + reAuthRetried = false, ): Promise> { let response: GaxiosResponse; try { @@ -343,7 +343,7 @@ export class DownscopedClient extends AuthClient { const stsResponse = await this.stsCredential.exchangeToken( stsCredentialsOptions, undefined, - this.credentialAccessBoundary + this.credentialAccessBoundary, ); /** diff --git a/src/auth/executable-response.ts b/src/auth/executable-response.ts index db4acf56..1e65e918 100644 --- a/src/auth/executable-response.ts +++ b/src/auth/executable-response.ts @@ -106,12 +106,12 @@ export class ExecutableResponse { // Check that the required fields exist in the json response. if (!responseJson.version) { throw new InvalidVersionFieldError( - "Executable response must contain a 'version' field." + "Executable response must contain a 'version' field.", ); } if (responseJson.success === undefined) { throw new InvalidSuccessFieldError( - "Executable response must contain a 'success' field." + "Executable response must contain a 'success' field.", ); } @@ -131,7 +131,7 @@ export class ExecutableResponse { ) { throw new InvalidTokenTypeFieldError( "Executable response must contain a 'token_type' field when successful " + - `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.` + `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.`, ); } @@ -139,7 +139,7 @@ export class ExecutableResponse { if (this.tokenType === SAML_SUBJECT_TOKEN_TYPE) { if (!responseJson.saml_response) { throw new InvalidSubjectTokenError( - `Executable response must contain a 'saml_response' field when token_type=${SAML_SUBJECT_TOKEN_TYPE}.` + `Executable response must contain a 'saml_response' field when token_type=${SAML_SUBJECT_TOKEN_TYPE}.`, ); } this.subjectToken = responseJson.saml_response; @@ -147,7 +147,7 @@ export class ExecutableResponse { if (!responseJson.id_token) { throw new InvalidSubjectTokenError( "Executable response must contain a 'id_token' field when " + - `token_type=${OIDC_SUBJECT_TOKEN_TYPE1} or ${OIDC_SUBJECT_TOKEN_TYPE2}.` + `token_type=${OIDC_SUBJECT_TOKEN_TYPE1} or ${OIDC_SUBJECT_TOKEN_TYPE2}.`, ); } this.subjectToken = responseJson.id_token; @@ -156,12 +156,12 @@ export class ExecutableResponse { // Both code and message must be provided for unsuccessful responses. if (!responseJson.code) { throw new InvalidCodeFieldError( - "Executable response must contain a 'code' field when unsuccessful." + "Executable response must contain a 'code' field when unsuccessful.", ); } if (!responseJson.message) { throw new InvalidMessageFieldError( - "Executable response must contain a 'message' field when unsuccessful." + "Executable response must contain a 'message' field when unsuccessful.", ); } this.errorCode = responseJson.code; diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index ebc43923..93c33ef6 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -109,7 +109,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { */ async refreshToken( refreshToken: string, - headers?: HeadersInit + headers?: HeadersInit, ): Promise { const opts: GaxiosOptions = { ...ExternalAccountAuthorizedUserHandler.RETRY_CONFIG, @@ -138,7 +138,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { throw getErrorFromOAuthErrorResponse( error.response.data as OAuthErrorResponse, // Preserve other fields from the original error. - error + error, ); } // Request could fail before the server responds. @@ -228,14 +228,14 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { request(opts: GaxiosOptions, callback: BodyResponseCallback): void; request( opts: GaxiosOptions, - callback?: BodyResponseCallback + callback?: BodyResponseCallback, ): GaxiosPromise | void { if (callback) { this.requestAsync(opts).then( r => callback(null, r), e => { return callback(e, e.response); - } + }, ); } else { return this.requestAsync(opts); @@ -251,7 +251,7 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { */ protected async requestAsync( opts: GaxiosOptions, - reAuthRetried = false + reAuthRetried = false, ): Promise> { let response: GaxiosResponse; try { @@ -295,7 +295,7 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { // Refresh the access token using the refresh token. const refreshResponse = await this.externalAccountAuthorizedUserHandler.refreshToken( - this.refreshToken + this.refreshToken, ); this.cachedAccessToken = { diff --git a/src/auth/externalclient.ts b/src/auth/externalclient.ts index 635f4fd6..ea90a03c 100644 --- a/src/auth/externalclient.ts +++ b/src/auth/externalclient.ts @@ -49,7 +49,7 @@ export class ExternalAccountClient { 'directly via explicit constructors, eg. ' + 'new AwsClient(options), new IdentityPoolClient(options), new' + 'PluggableAuthClientOptions, or via ' + - 'new GoogleAuth(options).getClient()' + 'new GoogleAuth(options).getClient()', ); } @@ -63,7 +63,7 @@ export class ExternalAccountClient { * provided do not correspond to an external account credential. */ static fromJSON( - options: ExternalAccountClientOptions + options: ExternalAccountClientOptions, ): BaseExternalAccountClient | null { if (options && options.type === EXTERNAL_ACCOUNT_TYPE) { if ((options as AwsClientOptions).credential_source?.environment_id) { diff --git a/src/auth/filesubjecttokensupplier.ts b/src/auth/filesubjecttokensupplier.ts index 8882980c..a615ba46 100644 --- a/src/auth/filesubjecttokensupplier.ts +++ b/src/auth/filesubjecttokensupplier.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ExternalAccountSupplierContext} from './baseexternalclient'; import { SubjectTokenFormatType, SubjectTokenJsonResponse, @@ -74,9 +73,7 @@ export class FileSubjectTokenSupplier implements SubjectTokenSupplier { * {@link IdentityPoolClient}, contains the requested audience and subject * token type for the external account identity. Not used. */ - async getSubjectToken( - context: ExternalAccountSupplierContext - ): Promise { + async getSubjectToken(): Promise { // Make sure there is a file at the path. lstatSync will throw if there is // nothing there. let parsedFilePath = this.filePath; @@ -106,7 +103,7 @@ export class FileSubjectTokenSupplier implements SubjectTokenSupplier { } if (!subjectToken) { throw new Error( - 'Unable to parse the subject_token from the credential_source file' + 'Unable to parse the subject_token from the credential_source file', ); } return subjectToken; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index b55b9f39..eadd78bb 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -141,9 +141,6 @@ export interface GoogleAuthOptions { universeDomain?: string; } -export const CLOUD_SDK_CLIENT_ID = - '764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com'; - export const GoogleAuthExceptionMessages = { API_KEY_WITH_CREDENTIALS: 'API Keys and Credentials are mutually exclusive authentication methods and cannot be used together.', @@ -224,7 +221,7 @@ export class GoogleAuth { // Cannot use both API Key + Credentials if (this.apiKey && (this.jsonContent || this.clientOptions.credentials)) { throw new RangeError( - GoogleAuthExceptionMessages.API_KEY_WITH_CREDENTIALS + GoogleAuthExceptionMessages.API_KEY_WITH_CREDENTIALS, ); } @@ -358,7 +355,7 @@ export class GoogleAuth { */ async getUniverseDomain(): Promise { let universeDomain = originalOrCamelOptions(this.clientOptions).get( - 'universe_domain' + 'universe_domain', ); try { universeDomain ??= (await this.getClient()).universeDomain; @@ -389,11 +386,11 @@ export class GoogleAuth { getApplicationDefault(options: AuthClientOptions): Promise; getApplicationDefault( options: AuthClientOptions, - callback: ADCCallback + callback: ADCCallback, ): void; getApplicationDefault( optionsOrCallback: ADCCallback | AuthClientOptions = {}, - callback?: ADCCallback + callback?: ADCCallback, ): void | Promise { let options: AuthClientOptions | undefined; if (typeof optionsOrCallback === 'function') { @@ -404,7 +401,7 @@ export class GoogleAuth { if (callback) { this.getApplicationDefaultAsync(options).then( r => callback!(null, r.credential, r.projectId), - callback + callback, ); } else { return this.getApplicationDefaultAsync(options); @@ -412,7 +409,7 @@ export class GoogleAuth { } private async getApplicationDefaultAsync( - options: AuthClientOptions = {} + options: AuthClientOptions = {}, ): Promise { // If we've already got a cached credential, return it. // This will also preserve one's configured quota project, in case they @@ -461,7 +458,7 @@ export class GoogleAuth { async #prepareAndCacheClient( credential: AnyAuthClient | T, - quotaProjectIdOverride = process.env['GOOGLE_CLOUD_QUOTA_PROJECT'] || null + quotaProjectIdOverride = process.env['GOOGLE_CLOUD_QUOTA_PROJECT'] || null, ): Promise { const projectId = await this.getProjectIdOptional(); @@ -497,7 +494,7 @@ export class GoogleAuth { * @api private */ async _tryGetApplicationCredentialsFromEnvironmentVariable( - options?: AuthClientOptions + options?: AuthClientOptions, ): Promise { const credentialsPath = process.env['GOOGLE_APPLICATION_CREDENTIALS'] || @@ -508,7 +505,7 @@ export class GoogleAuth { try { return this._getApplicationCredentialsFromFilePath( credentialsPath, - options + options, ); } catch (e) { if (e instanceof Error) { @@ -525,7 +522,7 @@ export class GoogleAuth { * @api private */ async _tryGetApplicationCredentialsFromWellKnownFile( - options?: AuthClientOptions + options?: AuthClientOptions, ): Promise { // First, figure out the location of the file, depending upon the OS type. let location = null; @@ -544,7 +541,7 @@ export class GoogleAuth { location = path.join( location, 'gcloud', - 'application_default_credentials.json' + 'application_default_credentials.json', ); if (!fs.existsSync(location)) { location = null; @@ -557,7 +554,7 @@ export class GoogleAuth { // The file seems to exist. Try to use it. const client = await this._getApplicationCredentialsFromFilePath( location, - options + options, ); return client; } @@ -570,7 +567,7 @@ export class GoogleAuth { */ async _getApplicationCredentialsFromFilePath( filePath: string, - options: AuthClientOptions = {} + options: AuthClientOptions = {}, ): Promise { // Make sure the path looks like a string. if (!filePath || filePath.length === 0) { @@ -608,22 +605,22 @@ export class GoogleAuth { fromImpersonatedJSON(json: ImpersonatedJWTInput): Impersonated { if (!json) { throw new Error( - 'Must pass in a JSON object containing an impersonated refresh token' + 'Must pass in a JSON object containing an impersonated refresh token', ); } if (json.type !== IMPERSONATED_ACCOUNT_TYPE) { throw new Error( - `The incoming JSON object does not have the "${IMPERSONATED_ACCOUNT_TYPE}" type` + `The incoming JSON object does not have the "${IMPERSONATED_ACCOUNT_TYPE}" type`, ); } if (!json.source_credentials) { throw new Error( - 'The incoming JSON object does not contain a source_credentials field' + 'The incoming JSON object does not contain a source_credentials field', ); } if (!json.service_account_impersonation_url) { throw new Error( - 'The incoming JSON object does not contain a service_account_impersonation_url field' + 'The incoming JSON object does not contain a service_account_impersonation_url field', ); } @@ -635,19 +632,19 @@ export class GoogleAuth { * @see {@link https://github.com/googleapis/google-auth-library-nodejs/security/code-scanning/85} **/ throw new RangeError( - `Target principal is too long: ${json.service_account_impersonation_url}` + `Target principal is too long: ${json.service_account_impersonation_url}`, ); } // Extract service account from service_account_impersonation_url const targetPrincipal = /(?[^/]+):(generateAccessToken|generateIdToken)$/.exec( - json.service_account_impersonation_url + json.service_account_impersonation_url, )?.groups?.target; if (!targetPrincipal) { throw new RangeError( - `Cannot extract target principal from ${json.service_account_impersonation_url}` + `Cannot extract target principal from ${json.service_account_impersonation_url}`, ); } @@ -673,7 +670,7 @@ export class GoogleAuth { */ fromJSON( json: JWTInput | ImpersonatedJWTInput, - options: AuthClientOptions = {} + options: AuthClientOptions = {}, ): JSONClient { let client: JSONClient; @@ -720,7 +717,7 @@ export class GoogleAuth { */ private _cacheClientFromJSON( json: JWTInput | ImpersonatedJWTInput, - options?: AuthClientOptions + options?: AuthClientOptions, ): JSONClient { const client = this.fromJSON(json, options); @@ -739,17 +736,17 @@ export class GoogleAuth { fromStream(inputStream: stream.Readable, callback: CredentialCallback): void; fromStream( inputStream: stream.Readable, - options: AuthClientOptions + options: AuthClientOptions, ): Promise; fromStream( inputStream: stream.Readable, options: AuthClientOptions, - callback: CredentialCallback + callback: CredentialCallback, ): void; fromStream( inputStream: stream.Readable, optionsOrCallback: AuthClientOptions | CredentialCallback = {}, - callback?: CredentialCallback + callback?: CredentialCallback, ): Promise | void { let options: AuthClientOptions = {}; if (typeof optionsOrCallback === 'function') { @@ -760,7 +757,7 @@ export class GoogleAuth { if (callback) { this.fromStreamAsync(inputStream, options).then( r => callback!(null, r), - callback + callback, ); } else { return this.fromStreamAsync(inputStream, options); @@ -769,12 +766,12 @@ export class GoogleAuth { private fromStreamAsync( inputStream: stream.Readable, - options?: AuthClientOptions + options?: AuthClientOptions, ): Promise { return new Promise((resolve, reject) => { if (!inputStream) { throw new Error( - 'Must pass in a stream containing the Google auth settings.' + 'Must pass in a stream containing the Google auth settings.', ); } const chunks: string[] = []; @@ -944,10 +941,10 @@ export class GoogleAuth { */ getCredentials(): Promise; getCredentials( - callback: (err: Error | null, credentials?: CredentialBody) => void + callback: (err: Error | null, credentials?: CredentialBody) => void, ): void; getCredentials( - callback?: (err: Error | null, credentials?: CredentialBody) => void + callback?: (err: Error | null, credentials?: CredentialBody) => void, ): void | Promise { if (callback) { this.getCredentialsAsync().then(r => callback(null, r), callback); @@ -1029,7 +1026,7 @@ export class GoogleAuth { return credential; } else { const {credential} = await this.getApplicationDefaultAsync( - this.clientOptions + this.clientOptions, ); return credential; } @@ -1044,7 +1041,7 @@ export class GoogleAuth { 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.' + '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({targetAudience, idTokenProvider: client}); @@ -1140,7 +1137,7 @@ export class GoogleAuth { crypto: Crypto, emailOrUniqueId: string, data: string, - endpoint: string + endpoint: string, ): Promise { const url = new URL(endpoint + `${emailOrUniqueId}:signBlob`); const res = await this.request({ diff --git a/src/auth/iam.ts b/src/auth/iam.ts index 6e789b11..d097627f 100644 --- a/src/auth/iam.ts +++ b/src/auth/iam.ts @@ -27,7 +27,7 @@ export class IAMAuth { */ constructor( public selector: string, - public token: string + public token: string, ) { this.selector = selector; this.token = token; diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index fa2c1d00..bdc24ac7 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -116,7 +116,7 @@ export class IdentityPoolClient extends BaseExternalAccountClient { constructor( options: | IdentityPoolClientOptions - | SnakeToCamelObject + | SnakeToCamelObject, ) { super(options); @@ -126,12 +126,12 @@ export class IdentityPoolClient extends BaseExternalAccountClient { // Validate credential sourcing configuration. if (!credentialSource && !subjectTokenSupplier) { throw new Error( - 'A credential source or subject token supplier must be specified.' + 'A credential source or subject token supplier must be specified.', ); } if (credentialSource && subjectTokenSupplier) { throw new Error( - 'Only one of credential source or subject token supplier can be specified.' + 'Only one of credential source or subject token supplier can be specified.', ); } @@ -142,13 +142,13 @@ export class IdentityPoolClient extends BaseExternalAccountClient { const credentialSourceOpts = originalOrCamelOptions(credentialSource); const formatOpts = originalOrCamelOptions( - credentialSourceOpts.get('format') + credentialSourceOpts.get('format'), ); // Text is the default format type. const formatType = formatOpts.get('type') || 'text'; const formatSubjectTokenFieldName = formatOpts.get( - 'subject_token_field_name' + 'subject_token_field_name', ); if (formatType !== 'json' && formatType !== 'text') { @@ -156,7 +156,7 @@ export class IdentityPoolClient extends BaseExternalAccountClient { } if (formatType === 'json' && !formatSubjectTokenFieldName) { throw new Error( - 'Missing subject_token_field_name for JSON credential_source format' + 'Missing subject_token_field_name for JSON credential_source format', ); } @@ -165,7 +165,7 @@ export class IdentityPoolClient extends BaseExternalAccountClient { const headers = credentialSourceOpts.get('headers'); if (file && url) { throw new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.' + 'No valid Identity Pool "credential_source" provided, must be either file or url.', ); } else if (file && !url) { this.credentialSourceType = 'file'; @@ -185,7 +185,7 @@ export class IdentityPoolClient extends BaseExternalAccountClient { }); } else { throw new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.' + 'No valid Identity Pool "credential_source" provided, must be either file or url.', ); } } diff --git a/src/auth/idtokenclient.ts b/src/auth/idtokenclient.ts index 67d8aca8..68303c97 100644 --- a/src/auth/idtokenclient.ts +++ b/src/auth/idtokenclient.ts @@ -57,7 +57,7 @@ export class IdTokenClient extends OAuth2Client { this.isTokenExpiring() ) { const idToken = await this.idTokenProvider.fetchIdToken( - this.targetAudience + this.targetAudience, ); this.credentials = { id_token: idToken, @@ -75,7 +75,7 @@ export class IdTokenClient extends OAuth2Client { const payloadB64 = idToken.split('.')[1]; if (payloadB64) { const payload = JSON.parse( - Buffer.from(payloadB64, 'base64').toString('ascii') + Buffer.from(payloadB64, 'base64').toString('ascii'), ); return payload.exp * 1000; } diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index 0dc0e7ec..04d52cfb 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -135,7 +135,7 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { } else if (this.sourceClient.universeDomain !== this.universeDomain) { // non-default universe and is not matching the source - this could be a credential leak throw new RangeError( - `Universe domain ${this.sourceClient.universeDomain} in source credentials does not match ${this.universeDomain} universe domain set for impersonated credentials.` + `Universe domain ${this.sourceClient.universeDomain} in source credentials does not match ${this.universeDomain} universe domain set for impersonated credentials.`, ); } @@ -231,7 +231,7 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { */ async fetchIdToken( targetAudience: string, - options?: FetchIdTokenOptions + options?: FetchIdTokenOptions, ): Promise { await this.sourceClient.getAccessToken(); diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index 2872efbe..57089fef 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -53,7 +53,7 @@ export class JWTAccess { email?: string | null, key?: string | null, keyId?: string | null, - eagerRefreshThresholdMillis?: number + eagerRefreshThresholdMillis?: number, ) { this.email = email; this.key = key; @@ -95,7 +95,7 @@ export class JWTAccess { getRequestHeaders( url?: string, additionalClaims?: Claims, - scopes?: string | string[] + scopes?: string | string[], ): Headers { // Return cached authorization headers, unless we are within // eagerRefreshThresholdMillis ms of them expiring: @@ -146,7 +146,7 @@ export class JWTAccess { for (const claim in defaultClaims) { if (additionalClaims[claim]) { throw new Error( - `The '${claim}' property is not allowed when passing additionalClaims. This claim is included in the JWT by default.` + `The '${claim}' property is not allowed when passing additionalClaims. This claim is included in the JWT by default.`, ); } } @@ -185,17 +185,17 @@ export class JWTAccess { fromJSON(json: JWTInput): void { if (!json) { throw new Error( - 'Must pass in a JSON object containing the service account auth settings.' + 'Must pass in a JSON object containing the service account auth settings.', ); } if (!json.client_email) { throw new Error( - 'The incoming JSON object does not contain a client_email field' + 'The incoming JSON object does not contain a client_email field', ); } if (!json.private_key) { throw new Error( - 'The incoming JSON object does not contain a private_key field' + 'The incoming JSON object does not contain a private_key field', ); } // Extract the relevant information from the json key file. @@ -213,11 +213,11 @@ export class JWTAccess { fromStream(inputStream: stream.Readable): Promise; fromStream( inputStream: stream.Readable, - callback: (err?: Error) => void + callback: (err?: Error) => void, ): void; fromStream( inputStream: stream.Readable, - callback?: (err?: Error) => void + callback?: (err?: Error) => void, ): void | Promise { if (callback) { this.fromStreamAsync(inputStream).then(() => callback(), callback); @@ -231,8 +231,8 @@ export class JWTAccess { if (!inputStream) { reject( new Error( - 'Must pass in a stream containing the service account auth settings.' - ) + 'Must pass in a stream containing the service account auth settings.', + ), ); } let s = ''; diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index c6c308f5..7ce7e45d 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -116,7 +116,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { * @param url the URI being authorized. */ protected async getRequestMetadataAsync( - url?: string | null + url?: string | null, ): Promise { url = this.defaultServicePath ? `https://${this.defaultServicePath}/` : url; const useSelfSignedJWT = @@ -126,7 +126,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { if (this.subject && this.universeDomain !== DEFAULT_UNIVERSE) { throw new RangeError( - `Service Account user is configured for the credential. Domain-wide delegation is not supported in universes other than ${DEFAULT_UNIVERSE}` + `Service Account user is configured for the credential. Domain-wide delegation is not supported in universes other than ${DEFAULT_UNIVERSE}`, ); } @@ -144,7 +144,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { headers: this.addSharedMetadataHeaders( new Headers({ authorization: `Bearer ${tokens.id_token}`, - }) + }), ), }; } else { @@ -155,7 +155,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { this.email, this.key, this.keyId, - this.eagerRefreshThresholdMillis + this.eagerRefreshThresholdMillis, ); } @@ -176,7 +176,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { // Scopes take precedent over audience for signing, // so we only provide them if `useJWTAccessWithScope` is on or // if we are in a non-default universe - useScopes ? scopes : undefined + useScopes ? scopes : undefined, ); return {headers: this.addSharedMetadataHeaders(headers)}; @@ -241,7 +241,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { authorize(): Promise; authorize(callback: (err: Error | null, result?: Credentials) => void): void; authorize( - callback?: (err: Error | null, result?: Credentials) => void + callback?: (err: Error | null, result?: Credentials) => void, ): Promise | void { if (callback) { this.authorizeAsync().then(r => callback(null, r), callback); @@ -267,10 +267,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { * @param refreshToken ignored * @private */ - protected async refreshTokenNoCache( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - refreshToken?: string | null - ): Promise { + protected async refreshTokenNoCache(): Promise { const gtoken = this.createGToken(); const token = await gtoken.getToken({ forceRefresh: this.isTokenExpiring(), @@ -314,17 +311,17 @@ export class JWT extends OAuth2Client implements IdTokenProvider { fromJSON(json: JWTInput): void { if (!json) { throw new Error( - 'Must pass in a JSON object containing the service account auth settings.' + 'Must pass in a JSON object containing the service account auth settings.', ); } if (!json.client_email) { throw new Error( - 'The incoming JSON object does not contain a client_email field' + 'The incoming JSON object does not contain a client_email field', ); } if (!json.private_key) { throw new Error( - 'The incoming JSON object does not contain a private_key field' + 'The incoming JSON object does not contain a private_key field', ); } // Extract the relevant information from the json key file. @@ -348,11 +345,11 @@ export class JWT extends OAuth2Client implements IdTokenProvider { fromStream(inputStream: stream.Readable): Promise; fromStream( inputStream: stream.Readable, - callback: (err?: Error | null) => void + callback: (err?: Error | null) => void, ): void; fromStream( inputStream: stream.Readable, - callback?: (err?: Error | null) => void + callback?: (err?: Error | null) => void, ): void | Promise { if (callback) { this.fromStreamAsync(inputStream).then(() => callback(), callback); @@ -365,7 +362,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { return new Promise((resolve, reject) => { if (!inputStream) { throw new Error( - 'Must pass in a stream containing the service account auth settings.' + 'Must pass in a stream containing the service account auth settings.', ); } let s = ''; diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index e9f903f1..5e2302ec 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -344,7 +344,7 @@ export interface GetTokenCallback { ( err: GaxiosError | null, token?: Credentials | null, - res?: GaxiosResponse | null + res?: GaxiosResponse | null, ): void; } @@ -357,7 +357,7 @@ export interface GetAccessTokenCallback { ( err: GaxiosError | null, token?: string | null, - res?: GaxiosResponse | null + res?: GaxiosResponse | null, ): void; } @@ -365,7 +365,7 @@ export interface RefreshAccessTokenCallback { ( err: GaxiosError | null, credentials?: Credentials | null, - res?: GaxiosResponse | null + res?: GaxiosResponse | null, ): void; } @@ -383,7 +383,7 @@ export interface RequestMetadataCallback { ( err: GaxiosError | null, headers?: Headers, - res?: GaxiosResponse | null + res?: GaxiosResponse | null, ): void; } @@ -391,7 +391,7 @@ export interface GetFederatedSignonCertsCallback { ( err: GaxiosError | null, certs?: Certificates, - response?: GaxiosResponse | null + response?: GaxiosResponse | null, ): void; } @@ -405,7 +405,7 @@ export interface GetIapPublicKeysCallback { ( err: GaxiosError | null, pubkeys?: PublicKeys, - response?: GaxiosResponse | null + response?: GaxiosResponse | null, ): void; } @@ -585,7 +585,7 @@ export class OAuth2Client extends AuthClient { /** * @deprecated - provide a {@link OAuth2ClientOptions `OAuth2ClientOptions`} object in the first parameter instead */ - redirectUri?: OAuth2ClientOptions['redirectUri'] + redirectUri?: OAuth2ClientOptions['redirectUri'], ) { super(typeof options === 'object' ? options : {}); @@ -647,7 +647,7 @@ export class OAuth2Client extends AuthClient { generateAuthUrl(opts: GenerateAuthUrlOpts = {}) { if (opts.code_challenge_method && !opts.code_challenge) { throw new Error( - 'If a code_challenge_method is provided, code_challenge must be included.' + 'If a code_challenge_method is provided, code_challenge must be included.', ); } opts.response_type = opts.response_type || 'code'; @@ -669,7 +669,7 @@ export class OAuth2Client extends AuthClient { // To make the code compatible with browser SubtleCrypto we need to make // this method async. throw new Error( - 'generateCodeVerifier is removed, please use generateCodeVerifierAsync instead.' + 'generateCodeVerifier is removed, please use generateCodeVerifierAsync instead.', ); } @@ -715,14 +715,14 @@ export class OAuth2Client extends AuthClient { getToken(options: GetTokenOptions, callback: GetTokenCallback): void; getToken( codeOrOptions: string | GetTokenOptions, - callback?: GetTokenCallback + callback?: GetTokenCallback, ): Promise | void { const options = typeof codeOrOptions === 'string' ? {code: codeOrOptions} : codeOrOptions; if (callback) { this.getTokenAsync(options).then( r => callback(null, r.tokens, r.res), - e => callback(e, null, e.response) + e => callback(e, null, e.response), ); } else { return this.getTokenAsync(options); @@ -730,7 +730,7 @@ export class OAuth2Client extends AuthClient { } private async getTokenAsync( - options: GetTokenOptions + options: GetTokenOptions, ): Promise { const url = this.endpoints.oauth2TokenUrl.toString(); const headers = new Headers(); @@ -771,7 +771,7 @@ export class OAuth2Client extends AuthClient { * @private */ protected async refreshToken( - refreshToken?: string | null + refreshToken?: string | null, ): Promise { if (!refreshToken) { return this.refreshTokenNoCache(refreshToken); @@ -790,14 +790,14 @@ export class OAuth2Client extends AuthClient { e => { this.refreshTokenPromises.delete(refreshToken); throw e; - } + }, ); this.refreshTokenPromises.set(refreshToken, p); return p; } protected async refreshTokenNoCache( - refreshToken?: string | null + refreshToken?: string | null, ): Promise { if (!refreshToken) { throw new Error('No refresh token is set.'); @@ -851,12 +851,12 @@ export class OAuth2Client extends AuthClient { refreshAccessToken(): Promise; refreshAccessToken(callback: RefreshAccessTokenCallback): void; refreshAccessToken( - callback?: RefreshAccessTokenCallback + callback?: RefreshAccessTokenCallback, ): Promise | void { if (callback) { this.refreshAccessTokenAsync().then( r => callback(null, r.credentials, r.res), - callback + callback, ); } else { return this.refreshAccessTokenAsync(); @@ -879,12 +879,12 @@ export class OAuth2Client extends AuthClient { getAccessToken(): Promise; getAccessToken(callback: GetAccessTokenCallback): void; getAccessToken( - callback?: GetAccessTokenCallback + callback?: GetAccessTokenCallback, ): Promise | void { if (callback) { this.getAccessTokenAsync().then( r => callback(null, r.token, r.res), - callback + callback, ); } else { return this.getAccessTokenAsync(); @@ -905,7 +905,7 @@ export class OAuth2Client extends AuthClient { } } else { throw new Error( - 'No refresh token or refresh handler callback is set.' + 'No refresh token or refresh handler callback is set.', ); } } @@ -927,7 +927,6 @@ export class OAuth2Client extends AuthClient { * * In OAuth2Client, the result has the form: * { authorization: 'Bearer ' } - * @param url The optional url being authorized */ async getRequestHeaders(url?: string | URL): Promise { const headers = (await this.getRequestMetadataAsync(url)).headers; @@ -935,9 +934,9 @@ export class OAuth2Client extends AuthClient { } protected async getRequestMetadataAsync( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - url?: string | URL | null + url?: string | URL | null, ): Promise { + url; const thisCreds = this.credentials; if ( !thisCreds.access_token && @@ -946,7 +945,7 @@ export class OAuth2Client extends AuthClient { !this.refreshHandler ) { throw new Error( - 'No access, refresh token, API key or refresh handler callback is set.' + 'No access, refresh token, API key or refresh handler callback is set.', ); } @@ -1031,11 +1030,11 @@ export class OAuth2Client extends AuthClient { revokeToken(token: string): GaxiosPromise; revokeToken( token: string, - callback: BodyResponseCallback + callback: BodyResponseCallback, ): void; revokeToken( token: string, - callback?: BodyResponseCallback + callback?: BodyResponseCallback, ): GaxiosPromise | void { const opts: GaxiosOptions = { ...OAuth2Client.RETRY_CONFIG, @@ -1057,10 +1056,10 @@ export class OAuth2Client extends AuthClient { */ revokeCredentials(): GaxiosPromise; revokeCredentials( - callback: BodyResponseCallback + callback: BodyResponseCallback, ): void; revokeCredentials( - callback?: BodyResponseCallback + callback?: BodyResponseCallback, ): GaxiosPromise | void { if (callback) { this.revokeCredentialsAsync().then(res => callback(null, res), callback); @@ -1091,14 +1090,14 @@ export class OAuth2Client extends AuthClient { request(opts: GaxiosOptions, callback: BodyResponseCallback): void; request( opts: GaxiosOptions, - callback?: BodyResponseCallback + callback?: BodyResponseCallback, ): GaxiosPromise | void { if (callback) { this.requestAsync(opts).then( r => callback(null, r), e => { return callback(e, e.response); - } + }, ); } else { return this.requestAsync(opts); @@ -1107,10 +1106,10 @@ export class OAuth2Client extends AuthClient { protected async requestAsync( opts: GaxiosOptions, - reAuthRetried = false + reAuthRetried = false, ): Promise> { try { - const r = await this.getRequestMetadataAsync(opts.url); + const r = await this.getRequestMetadataAsync(); opts.headers = Gaxios.mergeHeaders(opts.headers); this.addUserProjectAndAuthHeaders(opts.headers, r.headers); @@ -1192,18 +1191,18 @@ export class OAuth2Client extends AuthClient { verifyIdToken(options: VerifyIdTokenOptions): Promise; verifyIdToken( options: VerifyIdTokenOptions, - callback: (err: Error | null, login?: LoginTicket) => void + callback: (err: Error | null, login?: LoginTicket) => void, ): void; verifyIdToken( options: VerifyIdTokenOptions, - callback?: (err: Error | null, login?: LoginTicket) => void + callback?: (err: Error | null, login?: LoginTicket) => void, ): void | Promise { // This function used to accept two arguments instead of an options object. // Check the types to help users upgrade with less pain. // This check can be removed after a 2.0 release. if (callback && typeof callback !== 'function') { throw new Error( - 'This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry.' + 'This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry.', ); } @@ -1215,7 +1214,7 @@ export class OAuth2Client extends AuthClient { } private async verifyIdTokenAsync( - options: VerifyIdTokenOptions + options: VerifyIdTokenOptions, ): Promise { if (!options.idToken) { throw new Error('The verifyIdToken method requires an ID Token'); @@ -1226,7 +1225,7 @@ export class OAuth2Client extends AuthClient { response.certs, options.audience, this.issuers, - options.maxExpiry + options.maxExpiry, ); return login; @@ -1254,7 +1253,7 @@ export class OAuth2Client extends AuthClient { expiry_date: new Date().getTime() + data.expires_in! * 1000, scopes: data.scope!.split(' '), }, - data + data, ); delete info.expires_in; delete info.scope; @@ -1270,12 +1269,12 @@ export class OAuth2Client extends AuthClient { getFederatedSignonCerts(): Promise; getFederatedSignonCerts(callback: GetFederatedSignonCertsCallback): void; getFederatedSignonCerts( - callback?: GetFederatedSignonCertsCallback + callback?: GetFederatedSignonCertsCallback, ): Promise | void { if (callback) { this.getFederatedSignonCertsAsync().then( r => callback(null, r.certs, r.res), - callback + callback, ); } else { return this.getFederatedSignonCertsAsync(); @@ -1361,12 +1360,12 @@ export class OAuth2Client extends AuthClient { getIapPublicKeys(): Promise; getIapPublicKeys(callback: GetIapPublicKeysCallback): void; getIapPublicKeys( - callback?: GetIapPublicKeysCallback + callback?: GetIapPublicKeysCallback, ): Promise | void { if (callback) { this.getIapPublicKeysAsync().then( r => callback(null, r.pubkeys, r.res), - callback + callback, ); } else { return this.getIapPublicKeysAsync(); @@ -1397,7 +1396,7 @@ export class OAuth2Client extends AuthClient { // To make the code compatible with browser SubtleCrypto we need to make // this method async. throw new Error( - 'verifySignedJwtWithCerts is removed, please use verifySignedJwtWithCertsAsync instead.' + 'verifySignedJwtWithCerts is removed, please use verifySignedJwtWithCertsAsync instead.', ); } @@ -1416,7 +1415,7 @@ export class OAuth2Client extends AuthClient { certs: Certificates | PublicKeys, requiredAudience?: string | string[], issuers?: string[], - maxExpiry?: number + maxExpiry?: number, ) { const crypto = createCrypto(); @@ -1484,7 +1483,7 @@ export class OAuth2Client extends AuthClient { if (!payload.exp) { throw new Error( - 'No expiration time in token: ' + JSON.stringify(payload) + 'No expiration time in token: ' + JSON.stringify(payload), ); } @@ -1498,7 +1497,7 @@ export class OAuth2Client extends AuthClient { if (exp >= now + maxExpiry) { throw new Error( - 'Expiration time too far in future: ' + JSON.stringify(payload) + 'Expiration time too far in future: ' + JSON.stringify(payload), ); } @@ -1512,7 +1511,7 @@ export class OAuth2Client extends AuthClient { ' < ' + earliest + ': ' + - JSON.stringify(payload) + JSON.stringify(payload), ); } @@ -1523,7 +1522,7 @@ export class OAuth2Client extends AuthClient { ' > ' + latest + ': ' + - JSON.stringify(payload) + JSON.stringify(payload), ); } @@ -1532,7 +1531,7 @@ export class OAuth2Client extends AuthClient { 'Invalid issuer, expected one of [' + issuers + '], but got ' + - payload.iss + payload.iss, ); } @@ -1549,7 +1548,7 @@ export class OAuth2Client extends AuthClient { } if (!audVerified) { throw new Error( - 'Wrong recipient, payload audience != requiredAudience' + 'Wrong recipient, payload audience != requiredAudience', ); } } @@ -1568,7 +1567,7 @@ export class OAuth2Client extends AuthClient { const accessTokenResponse = await this.refreshHandler(); if (!accessTokenResponse.access_token) { throw new Error( - 'No access token is returned by the refreshHandler callback.' + 'No access token is returned by the refreshHandler callback.', ); } return accessTokenResponse; diff --git a/src/auth/oauth2common.ts b/src/auth/oauth2common.ts index c43ff78a..3b1de441 100644 --- a/src/auth/oauth2common.ts +++ b/src/auth/oauth2common.ts @@ -107,7 +107,7 @@ export abstract class OAuthClientAuthHandler { */ protected applyClientAuthenticationOptions( opts: GaxiosOptions, - bearerToken?: string + bearerToken?: string, ) { opts.headers = Gaxios.mergeHeaders(opts.headers); @@ -130,7 +130,7 @@ export abstract class OAuthClientAuthHandler { */ private injectAuthenticatedHeaders( opts: GaxiosOptions, - bearerToken?: string + bearerToken?: string, ) { // Bearer token prioritized higher than basic Auth. if (bearerToken) { @@ -142,7 +142,7 @@ export abstract class OAuthClientAuthHandler { const clientId = this.#clientAuthentication!.clientId; const clientSecret = this.#clientAuthentication!.clientSecret || ''; const base64EncodedCreds = this.#crypto.encodeBase64StringUtf8( - `${clientId}:${clientSecret}` + `${clientId}:${clientSecret}`, ); Gaxios.mergeHeaders(opts.headers, { authorization: `Basic ${base64EncodedCreds}`, @@ -165,7 +165,7 @@ export abstract class OAuthClientAuthHandler { throw new Error( `${method} HTTP method does not support ` + `${this.#clientAuthentication!.confidentialClientType} ` + - 'client authentication' + 'client authentication', ); } @@ -182,7 +182,7 @@ export abstract class OAuthClientAuthHandler { data.append('client_id', this.#clientAuthentication!.clientId); data.append( 'client_secret', - this.#clientAuthentication!.clientSecret || '' + this.#clientAuthentication!.clientSecret || '', ); opts.data = data; } else if (contentType?.startsWith('application/json')) { @@ -195,7 +195,7 @@ export abstract class OAuthClientAuthHandler { throw new Error( `${contentType} content-types are not supported with ` + `${this.#clientAuthentication!.confidentialClientType} ` + - 'client authentication' + 'client authentication', ); } } @@ -229,7 +229,7 @@ export abstract class OAuthClientAuthHandler { */ export function getErrorFromOAuthErrorResponse( resp: OAuthErrorResponse, - err?: Error + err?: Error, ): Error { // Error response. const errorCode = resp.error; @@ -254,8 +254,7 @@ export function getErrorFromOAuthErrorResponse( // Do not overwrite the message field. if (key !== 'message') { Object.defineProperty(newError, key, { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: (err! as {[index: string]: any})[key], + value: (err as {} as {[index: string]: string})[key], writable: false, enumerable: true, }); diff --git a/src/auth/passthrough.ts b/src/auth/passthrough.ts index a1515194..11949367 100644 --- a/src/auth/passthrough.ts +++ b/src/auth/passthrough.ts @@ -58,7 +58,3 @@ export class PassThroughClient extends AuthClient { return new Headers(); } } - -const a = new PassThroughClient(); - -a.getAccessToken(); diff --git a/src/auth/pluggable-auth-client.ts b/src/auth/pluggable-auth-client.ts index f1539321..142698fb 100644 --- a/src/auth/pluggable-auth-client.ts +++ b/src/auth/pluggable-auth-client.ts @@ -193,7 +193,7 @@ export class PluggableAuthClient extends BaseExternalAccountClient { ) { throw new Error( `Timeout must be between ${MINIMUM_EXECUTABLE_TIMEOUT_MILLIS} and ` + - `${MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS} milliseconds.` + `${MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS} milliseconds.`, ); } } @@ -231,7 +231,7 @@ export class PluggableAuthClient extends BaseExternalAccountClient { throw new Error( 'Pluggable Auth executables need to be explicitly allowed to run by ' + 'setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment ' + - 'Variable to 1.' + 'Variable to 1.', ); } @@ -255,7 +255,7 @@ export class PluggableAuthClient extends BaseExternalAccountClient { if (serviceAccountEmail) { envMap.set( 'GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL', - serviceAccountEmail + serviceAccountEmail, ); } executableResponse = @@ -264,21 +264,21 @@ export class PluggableAuthClient extends BaseExternalAccountClient { if (executableResponse.version > MAXIMUM_EXECUTABLE_VERSION) { throw new Error( - `Version of executable is not currently supported, maximum supported version is ${MAXIMUM_EXECUTABLE_VERSION}.` + `Version of executable is not currently supported, maximum supported version is ${MAXIMUM_EXECUTABLE_VERSION}.`, ); } // Check that response was successful. if (!executableResponse.success) { throw new ExecutableError( executableResponse.errorMessage as string, - executableResponse.errorCode as string + executableResponse.errorCode as string, ); } // Check that response contains expiration time if output file was specified. if (this.outputFile) { if (!executableResponse.expirationTime) { throw new InvalidExpirationTimeFieldError( - 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.' + 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.', ); } } diff --git a/src/auth/pluggable-auth-handler.ts b/src/auth/pluggable-auth-handler.ts index b56af076..c737075e 100644 --- a/src/auth/pluggable-auth-handler.ts +++ b/src/auth/pluggable-auth-handler.ts @@ -31,7 +31,7 @@ export class ExecutableError extends Error { constructor(message: string, code: string) { super( - `The executable failed with exit code: ${code} and error message: ${message}.` + `The executable failed with exit code: ${code} and error message: ${message}.`, ); this.code = code; Object.setPrototypeOf(this, new.target.prototype); @@ -75,7 +75,7 @@ export class PluggableAuthHandler { throw new Error('No command provided.'); } this.commandComponents = PluggableAuthHandler.parseCommand( - options.command + options.command, ) as Array; this.timeoutMillis = options.timeoutMillis; if (!this.timeoutMillis) { @@ -92,7 +92,7 @@ export class PluggableAuthHandler { * @return A promise that resolves with the executable response. */ retrieveResponseFromExecutable( - envMap: Map + envMap: Map, ): Promise { return new Promise((resolve, reject) => { // Spawn process to run executable using added environment variables. @@ -101,7 +101,7 @@ export class PluggableAuthHandler { this.commandComponents.slice(1), { env: {...process.env, ...Object.fromEntries(envMap)}, - } + }, ); let output = ''; // Append stdout to output as executable runs. @@ -121,8 +121,8 @@ export class PluggableAuthHandler { child.kill(); return reject( new Error( - 'The executable failed to finish within the timeout specified.' - ) + 'The executable failed to finish within the timeout specified.', + ), ); }, this.timeoutMillis); @@ -141,8 +141,8 @@ export class PluggableAuthHandler { } return reject( new ExecutableResponseError( - `The executable returned an invalid response: ${output}` - ) + `The executable returned an invalid response: ${output}`, + ), ); } } else { @@ -196,7 +196,7 @@ export class PluggableAuthHandler { throw error; } throw new ExecutableResponseError( - `The output file contained an invalid response: ${responseString}` + `The output file contained an invalid response: ${responseString}`, ); } } diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index 48ad7dea..e9f3387b 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -19,7 +19,6 @@ import { OAuth2Client, OAuth2ClientOptions, } from './oauth2client'; -import {stringify} from 'querystring'; export const USER_REFRESH_ACCOUNT_TYPE = 'authorized_user'; @@ -70,7 +69,7 @@ export class UserRefreshClient extends OAuth2Client { /** * @deprecated - provide a {@link UserRefreshClientOptions `UserRefreshClientOptions`} object in the first parameter instead */ - forceRefreshOnFailure?: UserRefreshClientOptions['forceRefreshOnFailure'] + forceRefreshOnFailure?: UserRefreshClientOptions['forceRefreshOnFailure'], ) { const opts = optionsOrClientId && typeof optionsOrClientId === 'object' @@ -92,10 +91,7 @@ export class UserRefreshClient extends OAuth2Client { * @param refreshToken An ignored refreshToken.. * @param callback Optional callback. */ - protected async refreshTokenNoCache( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - refreshToken?: string | null - ): Promise { + protected async refreshTokenNoCache(): Promise { return super.refreshTokenNoCache(this._refreshToken); } @@ -124,27 +120,27 @@ export class UserRefreshClient extends OAuth2Client { fromJSON(json: JWTInput): void { if (!json) { throw new Error( - 'Must pass in a JSON object containing the user refresh token' + 'Must pass in a JSON object containing the user refresh token', ); } if (json.type !== 'authorized_user') { throw new Error( - 'The incoming JSON object does not have the "authorized_user" type' + 'The incoming JSON object does not have the "authorized_user" type', ); } if (!json.client_id) { throw new Error( - 'The incoming JSON object does not contain a client_id field' + 'The incoming JSON object does not contain a client_id field', ); } if (!json.client_secret) { throw new Error( - 'The incoming JSON object does not contain a client_secret field' + 'The incoming JSON object does not contain a client_secret field', ); } if (!json.refresh_token) { throw new Error( - 'The incoming JSON object does not contain a refresh_token field' + 'The incoming JSON object does not contain a refresh_token field', ); } this._clientId = json.client_id; @@ -164,11 +160,11 @@ export class UserRefreshClient extends OAuth2Client { fromStream(inputStream: stream.Readable): Promise; fromStream( inputStream: stream.Readable, - callback: (err?: Error) => void + callback: (err?: Error) => void, ): void; fromStream( inputStream: stream.Readable, - callback?: (err?: Error) => void + callback?: (err?: Error) => void, ): void | Promise { if (callback) { this.fromStreamAsync(inputStream).then(() => callback(), callback); @@ -181,7 +177,7 @@ export class UserRefreshClient extends OAuth2Client { return new Promise((resolve, reject) => { if (!inputStream) { return reject( - new Error('Must pass in a stream containing the user refresh token.') + new Error('Must pass in a stream containing the user refresh token.'), ); } let s = ''; diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index fde2704e..6b1c91c9 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -157,7 +157,7 @@ export class StsCredentials extends OAuthClientAuthHandler { /** * @deprecated - provide a {@link StsCredentialsConstructionOptions `StsCredentialsConstructionOptions`} object in the first parameter instead */ - clientAuthentication?: ClientAuthentication + clientAuthentication?: ClientAuthentication, ) { if (typeof options !== 'object' || options instanceof URL) { options = { @@ -187,7 +187,7 @@ export class StsCredentials extends OAuthClientAuthHandler { async exchangeToken( stsCredentialsOptions: StsCredentialsOptions, headers?: HeadersInit, - options?: Parameters[0] + options?: Parameters[0], ): Promise { const values: StsRequestOptions = { grant_type: stsCredentialsOptions.grantType, @@ -234,7 +234,7 @@ export class StsCredentials extends OAuthClientAuthHandler { throw getErrorFromOAuthErrorResponse( error.response.data as OAuthErrorResponse, // Preserve other fields from the original error. - error + error, ); } // Request could fail before the server responds. diff --git a/src/auth/urlsubjecttokensupplier.ts b/src/auth/urlsubjecttokensupplier.ts index cc9b5a18..1d41089e 100644 --- a/src/auth/urlsubjecttokensupplier.ts +++ b/src/auth/urlsubjecttokensupplier.ts @@ -80,7 +80,7 @@ export class UrlSubjectTokenSupplier implements SubjectTokenSupplier { * token type for the external account identity. Not used. */ async getSubjectToken( - context: ExternalAccountSupplierContext + context: ExternalAccountSupplierContext, ): Promise { const opts: GaxiosOptions = { ...this.additionalGaxiosOptions, @@ -99,7 +99,7 @@ export class UrlSubjectTokenSupplier implements SubjectTokenSupplier { } if (!subjectToken) { throw new Error( - 'Unable to parse the subject_token from the credential_source URL' + 'Unable to parse the subject_token from the credential_source URL', ); } return subjectToken; diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index 0a263466..0656a6f9 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -28,7 +28,7 @@ export class BrowserCrypto implements Crypto { window.crypto.subtle === undefined ) { throw new Error( - "SubtleCrypto not found. Make sure it's an https:// website." + "SubtleCrypto not found. Make sure it's an https:// website.", ); } } @@ -44,7 +44,7 @@ export class BrowserCrypto implements Crypto { // Result is ArrayBuffer as well. const outputBuffer = await window.crypto.subtle.digest( 'SHA-256', - inputBuffer + inputBuffer, ); return base64js.fromByteArray(new Uint8Array(outputBuffer)); @@ -67,7 +67,7 @@ export class BrowserCrypto implements Crypto { async verify( pubkey: JwkCertificate, data: string, - signature: string + signature: string, ): Promise { const algo = { name: 'RSASSA-PKCS1-v1_5', @@ -76,14 +76,14 @@ export class BrowserCrypto implements Crypto { const dataArray = new TextEncoder().encode(data); const signatureArray = base64js.toByteArray( - BrowserCrypto.padBase64(signature) + BrowserCrypto.padBase64(signature), ); const cryptoKey = await window.crypto.subtle.importKey( 'jwk', pubkey, algo, true, - ['verify'] + ['verify'], ); // SubtleCrypto's verify method is async so we must make @@ -92,7 +92,7 @@ export class BrowserCrypto implements Crypto { algo, cryptoKey, signatureArray, - dataArray + dataArray, ); return result; } @@ -109,7 +109,7 @@ export class BrowserCrypto implements Crypto { privateKey, algo, true, - ['sign'] + ['sign'], ); // SubtleCrypto's sign method is async so we must make @@ -147,7 +147,7 @@ export class BrowserCrypto implements Crypto { // Result is ArrayBuffer as well. const outputBuffer = await window.crypto.subtle.digest( 'SHA-256', - inputBuffer + inputBuffer, ); return fromArrayBufferToHex(outputBuffer); @@ -163,7 +163,7 @@ export class BrowserCrypto implements Crypto { */ async signWithHmacSha256( key: string | ArrayBuffer, - msg: string + msg: string, ): Promise { // Convert key, if provided in ArrayBuffer format, to string. const rawKey = @@ -182,7 +182,7 @@ export class BrowserCrypto implements Crypto { }, }, false, - ['sign'] + ['sign'], ); return window.crypto.subtle.sign('HMAC', cryptoKey, enc.encode(msg)); } diff --git a/src/crypto/node/crypto.ts b/src/crypto/node/crypto.ts index e4113a6c..c0fbe193 100644 --- a/src/crypto/node/crypto.ts +++ b/src/crypto/node/crypto.ts @@ -28,7 +28,7 @@ export class NodeCrypto implements Crypto { async verify( pubkey: string, data: string | Buffer, - signature: string + signature: string, ): Promise { const verifier = crypto.createVerify('RSA-SHA256'); verifier.update(data); @@ -71,11 +71,11 @@ export class NodeCrypto implements Crypto { */ async signWithHmacSha256( key: string | ArrayBuffer, - msg: string + msg: string, ): Promise { const cryptoKey = typeof key === 'string' ? key : toBuffer(key); return toArrayBuffer( - crypto.createHmac('sha256', cryptoKey).update(msg).digest() + crypto.createHmac('sha256', cryptoKey).update(msg).digest(), ); } } @@ -89,7 +89,7 @@ export class NodeCrypto implements Crypto { function toArrayBuffer(buffer: Buffer): ArrayBuffer { return buffer.buffer.slice( buffer.byteOffset, - buffer.byteOffset + buffer.byteLength + buffer.byteOffset + buffer.byteLength, ); } diff --git a/src/crypto/shared.ts b/src/crypto/shared.ts index e95771be..12d0ebc6 100644 --- a/src/crypto/shared.ts +++ b/src/crypto/shared.ts @@ -27,11 +27,11 @@ export interface Crypto { verify( pubkey: string | JwkCertificate, data: string | Buffer, - signature: string + signature: string, ): Promise; sign( privateKey: string | JwkCertificate, - data: string | Buffer + data: string | Buffer, ): Promise; decodeBase64StringUtf8(base64: string): string; encodeBase64StringUtf8(text: string): string; @@ -53,7 +53,7 @@ export interface Crypto { */ signWithHmacSha256( key: string | ArrayBuffer, - msg: string + msg: string, ): Promise; } diff --git a/src/util.ts b/src/util.ts index ed4b92de..945137c5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -16,8 +16,6 @@ * A utility for converting snake_case to camelCase. * * For, for example `my_snake_string` becomes `mySnakeString`. - * - * @internal */ export type SnakeToCamel = S extends `${infer FirstWord}_${infer Remainder}` ? `${FirstWord}${Capitalize>}` @@ -56,8 +54,6 @@ export type SnakeToCamel = S extends `${infer FirstWord}_${infer Remainder}` * The generated documentation for the camelCase'd properties won't be available * until {@link https://github.com/microsoft/TypeScript/issues/50715} has been * resolved. - * - * @internal */ export type SnakeToCamelObject = { [K in keyof T as SnakeToCamel]: T[K] extends {} @@ -103,8 +99,6 @@ export type SnakeToCamelObject = { * resolved. * * Tracking: {@link https://github.com/googleapis/google-auth-library-nodejs/issues/1686} - * - * @internal */ export type OriginalAndCamel = { [K in keyof T as K | SnakeToCamel]: T[K] extends {} @@ -120,14 +114,12 @@ export type OriginalAndCamel = { * Match any `_` and not `_` pair, then return the uppercase of the not `_` * character. * - * @internal - * * @param str the string to convert * @returns the camelCase'd string */ export function snakeToCamel(str: T): SnakeToCamel { return str.replace(/([_][^_])/g, match => - match.slice(1).toUpperCase() + match.slice(1).toUpperCase(), ) as SnakeToCamel; } @@ -168,7 +160,6 @@ export interface LRUCacheOptions { * Not meant for external usage. * * @experimental - * @internal */ export class LRUCache { readonly capacity: number; diff --git a/system-test/fixtures/kitchen/package.json b/system-test/fixtures/kitchen/package.json index e075d89f..3e6a0f7c 100644 --- a/system-test/fixtures/kitchen/package.json +++ b/system-test/fixtures/kitchen/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.0.0", - "gts": "^5.0.0", + "gts": "^6.0.0", "null-loader": "^4.0.0", "ts-loader": "^8.0.0", "webpack": "^4.20.2", diff --git a/system-test/test.kitchen.ts b/system-test/test.kitchen.ts index 1eefd737..101b42ef 100644 --- a/system-test/test.kitchen.ts +++ b/system-test/test.kitchen.ts @@ -22,11 +22,11 @@ import * as path from 'path'; import {promisify} from 'util'; import {spawn} from 'child_process'; +import {pkg} from '../src/shared.cjs'; + const mvp = promisify(mv) as {} as (...args: string[]) => Promise; const ncpp = promisify(ncp); const keep = !!process.env.GALN_KEEP_TEMPDIRS; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const pkg = require('../../package.json'); let stagingDir: string; @@ -71,7 +71,7 @@ async function run(...params: Parameters) { async function packAndInstall() { stagingDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'google-auth-library-nodejs-pack-') + path.join(os.tmpdir(), 'google-auth-library-nodejs-pack-'), ); await run('npm', ['pack'], {}); diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 0f6f2f3d..09ca6ffa 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -22,6 +22,8 @@ import { ProjectInfo, } from '../src/auth/baseexternalclient'; +import {pkg} from '../src/shared.cjs'; + interface CloudRequestError { error: { code: number; @@ -33,8 +35,7 @@ interface CloudRequestError { interface NockMockStsToken { statusCode: number; response: StsSuccessfulResponse | OAuthErrorResponse; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - request: {[key: string]: any}; + request: {[key: string]: ReturnType}; } interface NockMockGenerateAccessToken { @@ -55,19 +56,16 @@ export const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; const saBaseUrl = 'https://iamcredentials.googleapis.com'; const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const pkg = require('../../package.json'); - export function mockStsTokenExchange( nockParams: NockMockStsToken[], additionalHeaders?: {[key: string]: string}, - baseURL = baseUrl + baseURL = baseUrl, ): nock.Scope { const headers = Object.assign( { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, - additionalHeaders || {} + additionalHeaders || {}, ); const scope = nock(baseURL, {reqheaders: headers}); nockParams.forEach(nockMockStsToken => { @@ -79,7 +77,7 @@ export function mockStsTokenExchange( } export function mockGenerateAccessToken( - nockMockGenerateAccessToken: NockMockGenerateAccessToken + nockMockGenerateAccessToken: NockMockGenerateAccessToken, ): nock.Scope { const token = nockMockGenerateAccessToken.token; const scope = nock(saBaseUrl, { @@ -95,13 +93,13 @@ export function mockGenerateAccessToken( }) .reply( nockMockGenerateAccessToken.statusCode, - nockMockGenerateAccessToken.response + nockMockGenerateAccessToken.response, ); return scope; } export function getAudience( - projectNumber: string = defaultProjectNumber + projectNumber: string = defaultProjectNumber, ): string { return ( `//iam.googleapis.com/projects/${projectNumber}` + @@ -127,7 +125,7 @@ export function mockCloudResourceManager( projectNumber: string, accessToken: string, statusCode: number, - response: ProjectInfo | CloudRequestError + response: ProjectInfo | CloudRequestError, ): nock.Scope { return nock('https://cloudresourcemanager.googleapis.com', { reqheaders: {authorization: `Bearer ${accessToken}`}, @@ -139,7 +137,7 @@ export function mockCloudResourceManager( export function getExpectedExternalAccountMetricsHeaderValue( expectedSource: string, expectedSaImpersonation: boolean, - expectedConfigLifetime: boolean + expectedConfigLifetime: boolean, ): string { const languageVersion = process.version.replace(/^v/, ''); return `gl-node/${languageVersion} auth/${pkg.version} google-byoid-sdk source/${expectedSource} sa-impersonation/${expectedSaImpersonation} config-lifetime/${expectedConfigLifetime}`; diff --git a/test/test.authclient.ts b/test/test.authclient.ts index 4674cf3b..f82d8644 100644 --- a/test/test.authclient.ts +++ b/test/test.authclient.ts @@ -49,7 +49,7 @@ describe('AuthClient', () => { new PassThroughClient({transporter: gaxios}); assert( - gaxios.interceptors.request.has(AuthClient.DEFAULT_REQUEST_INTERCEPTOR) + gaxios.interceptors.request.has(AuthClient.DEFAULT_REQUEST_INTERCEPTOR), ); }); @@ -65,7 +65,7 @@ describe('AuthClient', () => { assert.equal(authClient.transporter, gaxios); assert.equal( authClient.transporter.interceptors.request.size, - originalInterceptorCount + originalInterceptorCount, ); }); @@ -133,7 +133,7 @@ describe('AuthClient', () => { assert.equal( options.headers.get('x-goog-api-client'), - `gl-node/${process.version.replace(/^v/, '')}` + `gl-node/${process.version.replace(/^v/, '')}`, ); }); diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 9fbc847f..c36659fa 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -18,10 +18,7 @@ import * as nock from 'nock'; import * as sinon from 'sinon'; import {AwsClient, AwsSecurityCredentialsSupplier} from '../src/auth/awsclient'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; -import { - BaseExternalAccountClient, - ExternalAccountSupplierContext, -} from '../src/auth/baseexternalclient'; +import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; import { assertGaxiosResponsePresent, getAudience, @@ -62,7 +59,7 @@ describe('AwsClient', () => { }; const awsCredentialSourceWithImdsv2 = Object.assign( {imdsv2_session_token_url: `${metadataBaseUrl}/latest/api/token`}, - awsCredentialSource + awsCredentialSource, ); const awsOptions = { type: 'external_account', @@ -82,7 +79,7 @@ describe('AwsClient', () => { { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - awsOptions + awsOptions, ); const stsSuccessfulResponse: StsSuccessfulResponse = { access_token: 'ACCESS_TOKEN', @@ -134,7 +131,7 @@ describe('AwsClient', () => { value: awsOptions.audience, }, ], - }) + }), ); // Signature retrieved from "signed request when AWS credentials have no // token" test in test.awsclient.ts. @@ -175,7 +172,7 @@ describe('AwsClient', () => { value: awsOptions.audience, }, ], - }) + }), ); beforeEach(() => { @@ -201,7 +198,7 @@ describe('AwsClient', () => { requiredCredentialSourceFields.forEach(required => { it(`should throw when credential_source is missing ${required}`, () => { const expectedError = new Error( - 'No valid AWS "credential_source" provided' + 'No valid AWS "credential_source" provided', ); const invalidCredentialSource = Object.assign({}, awsCredentialSource); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -237,7 +234,7 @@ describe('AwsClient', () => { it('should throw when an unsupported environment ID is provided', () => { const expectedError = new Error( - 'No valid AWS "credential_source" provided' + 'No valid AWS "credential_source" provided', ); const invalidCredentialSource = Object.assign({}, awsCredentialSource); invalidCredentialSource.environment_id = 'azure1'; @@ -254,7 +251,7 @@ describe('AwsClient', () => { it('should throw when an unsupported environment version is provided', () => { const expectedError = new Error( - 'aws version "3" is not supported in the current build.' + 'aws version "3" is not supported in the current build.', ); const invalidCredentialSource = Object.assign({}, awsCredentialSource); invalidCredentialSource.environment_id = 'aws3'; @@ -271,7 +268,7 @@ describe('AwsClient', () => { it('should throw when both a credential source and supplier are provided', () => { const expectedError = new Error( - 'Only one of credential source or AWS security credentials supplier can be specified.' + 'Only one of credential source or AWS security credentials supplier can be specified.', ); const invalidOptions = { type: 'external_account', @@ -287,7 +284,7 @@ describe('AwsClient', () => { it('should throw when neither a credential source or supplier are provided', () => { const expectedError = new Error( - 'A credential source or AWS security credentials supplier must be specified.' + 'A credential source or AWS security credentials supplier must be specified.', ); const invalidOptions = { type: 'external_account', @@ -365,7 +362,7 @@ describe('AwsClient', () => { }) .put('/latest/api/token') .twice() - .reply(200, awsSessionToken) + .reply(200, awsSessionToken), ); scopes.push( @@ -377,7 +374,7 @@ describe('AwsClient', () => { .get('/latest/meta-data/iam/security-credentials') .reply(200, awsRole) .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) - .reply(200, awsSecurityCredentials) + .reply(200, awsSecurityCredentials), ); const client = new AwsClient(awsOptionsWithImdsv2); @@ -390,7 +387,7 @@ describe('AwsClient', () => { it('should resolve on success with permanent creds', async () => { const permanentAwsSecurityCredentials = Object.assign( {}, - awsSecurityCredentials + awsSecurityCredentials, ); delete permanentAwsSecurityCredentials.Token; const scope = nock(metadataBaseUrl) @@ -482,11 +479,11 @@ describe('AwsClient', () => { it('should reject when "credential_source.url" is missing', async () => { const expectedError = new Error( 'Unable to determine AWS role name due to missing ' + - '"options.credential_source.url"' + '"options.credential_source.url"', ); const missingUrlCredentialSource = Object.assign( {}, - awsCredentialSource + awsCredentialSource, ); delete ( missingUrlCredentialSource as Partial @@ -511,11 +508,11 @@ describe('AwsClient', () => { it('should reject when "credential_source.region_url" is missing', async () => { const expectedError = new RangeError( 'Unable to determine AWS region due to missing ' + - '"options.credential_source.region_url"' + '"options.credential_source.region_url"', ); const missingRegionUrlCredentialSource = Object.assign( {}, - awsCredentialSource + awsCredentialSource, ); delete ( missingRegionUrlCredentialSource as Partial< @@ -555,7 +552,7 @@ describe('AwsClient', () => { 'urn:ietf:params:aws:token-type:aws4_request', }, }, - ]) + ]), ); scopes.push( nock(metadataBaseUrl) @@ -564,7 +561,7 @@ describe('AwsClient', () => { .get('/latest/meta-data/iam/security-credentials') .reply(200, awsRole) .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) - .reply(200, awsSecurityCredentials) + .reply(200, awsSecurityCredentials), ); const client = new AwsClient(awsOptions); @@ -583,7 +580,7 @@ describe('AwsClient', () => { const saSuccessResponse = { accessToken: 'SA_ACCESS_TOKEN', expireTime: new Date( - referenceDate.getTime() + ONE_HOUR_IN_SECS * 1000 + referenceDate.getTime() + ONE_HOUR_IN_SECS * 1000, ).toISOString(), }; const scopes: nock.Scope[] = []; @@ -616,7 +613,7 @@ describe('AwsClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new AwsClient(awsOptionsWithSA); @@ -788,7 +785,7 @@ describe('AwsClient', () => { process.env.AWS_REGION = awsRegion; const requiredOnlyCredentialSource = Object.assign( {}, - awsCredentialSource + awsCredentialSource, ); // Remove all optional fields. delete ( @@ -821,7 +818,7 @@ describe('AwsClient', () => { reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }) .put('/latest/api/token') - .reply(200, awsSessionToken) + .reply(200, awsSessionToken), ); scopes.push( @@ -829,7 +826,7 @@ describe('AwsClient', () => { reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, }) .get('/latest/meta-data/placement/availability-zone') - .reply(200, `${awsRegion}b`) + .reply(200, `${awsRegion}b`), ); const client = new AwsClient(awsOptionsWithImdsv2); @@ -850,7 +847,7 @@ describe('AwsClient', () => { reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }) .put('/latest/api/token') - .reply(200, awsSessionToken) + .reply(200, awsSessionToken), ); scopes.push( @@ -858,7 +855,7 @@ describe('AwsClient', () => { reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, }) .get('/latest/meta-data/placement/availability-zone') - .reply(200, `${awsRegion}b`) + .reply(200, `${awsRegion}b`), ); const client = new AwsClient(awsOptionsWithImdsv2); @@ -888,7 +885,7 @@ describe('AwsClient', () => { reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }) .put('/latest/api/token') - .reply(200, awsSessionToken) + .reply(200, awsSessionToken), ); scopes.push( @@ -898,7 +895,7 @@ describe('AwsClient', () => { .get('/latest/meta-data/iam/security-credentials') .reply(200, awsRole) .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) - .reply(200, awsSecurityCredentials) + .reply(200, awsSecurityCredentials), ); const client = new AwsClient(awsOptionsWithImdsv2); @@ -918,7 +915,7 @@ describe('AwsClient', () => { reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }) .put('/latest/api/token') - .reply(200, awsSessionToken) + .reply(200, awsSessionToken), ); scopes.push( @@ -928,7 +925,7 @@ describe('AwsClient', () => { .get('/latest/meta-data/iam/security-credentials') .reply(200, awsRole) .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) - .reply(200, awsSecurityCredentials) + .reply(200, awsSecurityCredentials), ); const client = new AwsClient(awsOptionsWithImdsv2); @@ -948,7 +945,7 @@ describe('AwsClient', () => { reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }) .put('/latest/api/token') - .reply(200, awsSessionToken) + .reply(200, awsSessionToken), ); scopes.push( @@ -958,7 +955,7 @@ describe('AwsClient', () => { .get('/latest/meta-data/iam/security-credentials') .reply(200, awsRole) .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) - .reply(200, awsSecurityCredentials) + .reply(200, awsSecurityCredentials), ); const client = new AwsClient(awsOptionsWithImdsv2); @@ -988,12 +985,12 @@ describe('AwsClient', () => { 'urn:ietf:params:aws:token-type:aws4_request', }, }, - ]) + ]), ); scopes.push( nock(metadataBaseUrl) .get('/latest/meta-data/placement/availability-zone') - .reply(200, `${awsRegion}b`) + .reply(200, `${awsRegion}b`), ); process.env.AWS_ACCESS_KEY_ID = accessKeyId; process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; @@ -1050,15 +1047,15 @@ describe('AwsClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'aws', false, - false + false, ), - } - ) + }, + ), ); scopes.push( nock(metadataBaseUrl) .get('/latest/meta-data/placement/availability-zone') - .reply(200, `${awsRegion}b`) + .reply(200, `${awsRegion}b`), ); process.env.AWS_ACCESS_KEY_ID = accessKeyId; process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; @@ -1173,7 +1170,7 @@ describe('AwsClient', () => { 'urn:ietf:params:aws:token-type:aws4_request', }, }, - ]) + ]), ); const supplier = new TestAwsSupplier({ credentials: { @@ -1241,10 +1238,10 @@ describe('AwsClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'programmatic', false, - false + false, ), - } - ) + }, + ), ); const supplier = new TestAwsSupplier({ credentials: { @@ -1294,7 +1291,7 @@ class TestAwsSupplier implements AwsSecurityCredentialsSupplier { this.regionError = options.regionError; } - async getAwsRegion(context: ExternalAccountSupplierContext): Promise { + async getAwsRegion(): Promise { if (this.regionError) { throw this.regionError; } else { @@ -1302,9 +1299,7 @@ class TestAwsSupplier implements AwsSecurityCredentialsSupplier { } } - async getAwsSecurityCredentials( - context: ExternalAccountSupplierContext - ): Promise { + async getAwsSecurityCredentials(): Promise { if (this.credentialsError) { throw this.credentialsError; } else { diff --git a/test/test.awsrequestsigner.ts b/test/test.awsrequestsigner.ts index d675e4f8..197ffef8 100644 --- a/test/test.awsrequestsigner.ts +++ b/test/test.awsrequestsigner.ts @@ -700,7 +700,7 @@ describe('AwsRequestSigner', () => { it(`should resolve with the expected ${test.description}`, async () => { clock.tick(test.referenceDate.getTime()); const actualSignedRequest = await test.instance.getRequestOptions( - test.originalRequest + test.originalRequest, ); assert.deepStrictEqual(actualSignedRequest, test.getSignedRequest()); }); @@ -709,7 +709,7 @@ describe('AwsRequestSigner', () => { it('should reject with underlying getCredentials error', async () => { const awsRequestSigner = new AwsRequestSigner( getCredentialsUnsuccessful, - 'us-east-2' + 'us-east-2', ); const options: GaxiosOptions = { url: @@ -720,22 +720,22 @@ describe('AwsRequestSigner', () => { await assert.rejects( awsRequestSigner.getRequestOptions(options), - awsError + awsError, ); }); it('should reject when no URL is available', async () => { const invalidOptionsError = new RangeError( - '"url" is required in "amzOptions"' + '"url" is required in "amzOptions"', ); const awsRequestSigner = new AwsRequestSigner( getCredentials, - 'us-east-2' + 'us-east-2', ); await assert.rejects( awsRequestSigner.getRequestOptions({}), - invalidOptionsError + invalidOptionsError, ); }); }); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 34229748..e0820cba 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -105,7 +105,7 @@ describe('BaseExternalAccountClient', () => { audience: '//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider', subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - } + }, ); const externalAccountOptionsWithClientAuthAndWorkforceUserProject = Object.assign( @@ -113,7 +113,7 @@ describe('BaseExternalAccountClient', () => { client_id: 'CLIENT_ID', client_secret: 'SECRET', }, - externalAccountOptionsWorkforceUserProject + externalAccountOptionsWorkforceUserProject, ); const basicAuthCreds = `${externalAccountOptionsWithCreds.client_id}:` + @@ -129,19 +129,19 @@ describe('BaseExternalAccountClient', () => { { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - externalAccountOptions + externalAccountOptions, ); const externalAccountOptionsWithCredsAndSA = Object.assign( { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - externalAccountOptionsWithCreds + externalAccountOptionsWithCreds, ); const externalAccountOptionsWithWorkforceUserProjectAndSA = Object.assign( { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - externalAccountOptionsWorkforceUserProject + externalAccountOptionsWorkforceUserProject, ); const indeterminableProjectIdAudiences = [ // Legacy K8s audience format. @@ -165,7 +165,7 @@ describe('BaseExternalAccountClient', () => { describe('Constructor', () => { it('should throw on invalid type', () => { const expectedError = new Error( - 'Expected "external_account" type but received "invalid"' + 'Expected "external_account" type but received "invalid"', ); const invalidOptions = Object.assign({}, externalAccountOptions); invalidOptions.type = 'invalid'; @@ -192,11 +192,11 @@ describe('BaseExternalAccountClient', () => { ]; const invalidExternalAccountOptionsWorkforceUserProject = Object.assign( {}, - externalAccountOptionsWorkforceUserProject + externalAccountOptionsWorkforceUserProject, ); const expectedWorkforcePoolUserProjectError = new Error( 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' + 'credentials.', ); invalidWorkforceAudiences.forEach(invalidWorkforceAudience => { @@ -206,7 +206,7 @@ describe('BaseExternalAccountClient', () => { assert.throws(() => { return new TestExternalAccountClient( - invalidExternalAccountOptionsWorkforceUserProject + invalidExternalAccountOptionsWorkforceUserProject, ); }, expectedWorkforcePoolUserProjectError); }); @@ -220,7 +220,7 @@ describe('BaseExternalAccountClient', () => { ]; const validExternalAccountOptionsWorkforceUserProject = Object.assign( {}, - externalAccountOptionsWorkforceUserProject + externalAccountOptionsWorkforceUserProject, ); for (const validWorkforceAudience of validWorkforceAudiences) { validExternalAccountOptionsWorkforceUserProject.audience = @@ -228,7 +228,7 @@ describe('BaseExternalAccountClient', () => { assert.doesNotThrow(() => { return new TestExternalAccountClient( - validExternalAccountOptionsWorkforceUserProject + validExternalAccountOptionsWorkforceUserProject, ); }); } @@ -259,11 +259,11 @@ describe('BaseExternalAccountClient', () => { assert.strictEqual( client.forceRefreshOnFailure, - refreshOptions.forceRefreshOnFailure + refreshOptions.forceRefreshOnFailure, ); assert.strictEqual( client.eagerRefreshThresholdMillis, - refreshOptions.eagerRefreshThresholdMillis + refreshOptions.eagerRefreshThresholdMillis, ); }); @@ -315,7 +315,7 @@ describe('BaseExternalAccountClient', () => { }, ], {}, - 'https://sts.test.com' + 'https://sts.test.com', ); await client.getAccessToken(); @@ -392,7 +392,7 @@ describe('BaseExternalAccountClient', () => { it('should return null for workforce pools with workforce_pool_user_project', () => { const options = Object.assign( {}, - externalAccountOptionsWorkforceUserProject + externalAccountOptionsWorkforceUserProject, ); const client = new TestExternalAccountClient(options); @@ -446,7 +446,7 @@ describe('BaseExternalAccountClient', () => { const saPath = `/v1/projects/-/serviceAccounts/${saEmail}:generateAccessToken`; const options: BaseExternalAccountClientOptions = Object.assign( {}, - externalAccountOptions + externalAccountOptions, ); options.service_account_impersonation_url = `${saBaseUrl}${saPath}`; const client = new TestExternalAccountClient(options); @@ -457,7 +457,7 @@ describe('BaseExternalAccountClient', () => { it('should return null when impersonation is not used', () => { const options: BaseExternalAccountClientOptions = Object.assign( {}, - externalAccountOptions + externalAccountOptions, ); delete options.service_account_impersonation_url; const client = new TestExternalAccountClient(options); @@ -471,7 +471,7 @@ describe('BaseExternalAccountClient', () => { const saPath = '/v1/projects/-/serviceAccounts/:generateAccessToken'; const options: BaseExternalAccountClientOptions = Object.assign( {}, - externalAccountOptions + externalAccountOptions, ); options.service_account_impersonation_url = `${saBaseUrl}${saPath}`; const client = new TestExternalAccountClient(options); @@ -484,7 +484,7 @@ describe('BaseExternalAccountClient', () => { it('should resolve for workforce pools when workforce_pool_user_project is provided', async () => { const options = Object.assign( {}, - externalAccountOptionsWorkforceUserProject + externalAccountOptionsWorkforceUserProject, ); const projectNumber = options.workforce_pool_user_project; const projectId = 'my-proj-id'; @@ -523,7 +523,7 @@ describe('BaseExternalAccountClient', () => { options.workforce_pool_user_project, stsSuccessfulResponse.access_token, 200, - response + response, ), ]; @@ -576,7 +576,7 @@ describe('BaseExternalAccountClient', () => { projectNumber, stsSuccessfulResponse.access_token, 200, - response + response, ), ]; const client = new TestExternalAccountClient(options); @@ -624,7 +624,7 @@ describe('BaseExternalAccountClient', () => { projectNumber, stsSuccessfulResponse.access_token, 403, - response + response, ), ]; const client = new TestExternalAccountClient(options); @@ -698,13 +698,13 @@ describe('BaseExternalAccountClient', () => { ], { authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds + basicAuthCreds, )}`, - } + }, ); const client = new TestExternalAccountClient( - externalAccountOptionsWithClientAuthAndWorkforceUserProject + externalAccountOptionsWithClientAuthAndWorkforceUserProject, ); const actualResponse = await client.getAccessToken(); @@ -740,7 +740,7 @@ describe('BaseExternalAccountClient', () => { ]); const client = new TestExternalAccountClient( - externalAccountOptionsWorkforceUserProject + externalAccountOptionsWorkforceUserProject, ); const actualResponse = await client.getAccessToken(); @@ -773,19 +773,19 @@ describe('BaseExternalAccountClient', () => { ], { authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds + basicAuthCreds, )}`, - } + }, ); const externalAccountOptionsWithClientAuth: BaseExternalAccountClientOptions = Object.assign( {}, - externalAccountOptionsWithClientAuthAndWorkforceUserProject + externalAccountOptionsWithClientAuthAndWorkforceUserProject, ); delete externalAccountOptionsWithClientAuth.workforce_pool_user_project; const client = new TestExternalAccountClient( - externalAccountOptionsWithClientAuth + externalAccountOptionsWithClientAuth, ); const actualResponse = await client.getAccessToken(); @@ -820,7 +820,7 @@ describe('BaseExternalAccountClient', () => { ]); const client = new TestExternalAccountClient( - externalAccountOptionsWithCreds + externalAccountOptionsWithCreds, ); // Listen to tokens events. On every event, push to list of // emittedEvents. @@ -848,7 +848,7 @@ describe('BaseExternalAccountClient', () => { assert.deepStrictEqual(client.credentials.expiry_date, undefined); assert.deepStrictEqual( client.credentials.access_token, - stsSuccessfulResponse2.access_token + stsSuccessfulResponse2.access_token, ); scope.done(); }); @@ -891,7 +891,7 @@ describe('BaseExternalAccountClient', () => { const client = new TestExternalAccountClient(externalAccountOptions); await assert.rejects( client.getAccessToken(), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); // Next try should succeed. const actualResponse = await client.getAccessToken(); @@ -1154,13 +1154,13 @@ describe('BaseExternalAccountClient', () => { ], { authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds + basicAuthCreds, )}`, - } + }, ); const client = new TestExternalAccountClient( - externalAccountOptionsWithCreds + externalAccountOptionsWithCreds, ); const actualResponse = await client.getAccessToken(); @@ -1194,9 +1194,9 @@ describe('BaseExternalAccountClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'test', false, - false + false, ), - } + }, ); const client = new TestExternalAccountClient(externalAccountOptions); @@ -1243,7 +1243,7 @@ describe('BaseExternalAccountClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -1251,11 +1251,11 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new TestExternalAccountClient( - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); const actualResponse = await client.getAccessToken(); @@ -1300,7 +1300,7 @@ describe('BaseExternalAccountClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -1308,7 +1308,7 @@ describe('BaseExternalAccountClient', () => { response: saErrorResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); scopes.push( mockGenerateAccessToken({ @@ -1316,11 +1316,11 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse2.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new TestExternalAccountClient( - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); await assert.rejects(client.getAccessToken(), GaxiosError); // Next try should succeed. @@ -1351,7 +1351,7 @@ describe('BaseExternalAccountClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -1359,11 +1359,11 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['scope1', 'scope2'], - }) + }), ); const client = new TestExternalAccountClient( - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); // These scopes should be used for the iamcredentials call. // https://www.googleapis.com/auth/cloud-platform should be used for the @@ -1386,13 +1386,13 @@ describe('BaseExternalAccountClient', () => { const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; saSuccessResponse.expireTime = new Date( - ONE_HOUR_IN_SECS * 1000 + ONE_HOUR_IN_SECS * 1000, ).toISOString(); const saSuccessResponse2 = Object.assign({}, saSuccessResponse); saSuccessResponse2.accessToken = 'SA_ACCESS_TOKEN2'; const customExpirationInSecs = 1600; saSuccessResponse2.expireTime = new Date( - (ONE_HOUR_IN_SECS + customExpirationInSecs) * 1000 + (ONE_HOUR_IN_SECS + customExpirationInSecs) * 1000, ).toISOString(); const scopes: nock.Scope[] = []; scopes.push( @@ -1423,7 +1423,7 @@ describe('BaseExternalAccountClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -1431,7 +1431,7 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); scopes.push( mockGenerateAccessToken({ @@ -1439,11 +1439,11 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse2, token: stsSuccessfulResponse2.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new TestExternalAccountClient( - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); // Listen to tokens events. On every event, push to list of // emittedEvents. @@ -1510,12 +1510,12 @@ describe('BaseExternalAccountClient', () => { const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; saSuccessResponse.expireTime = new Date( - ONE_HOUR_IN_SECS * 1000 + ONE_HOUR_IN_SECS * 1000, ).toISOString(); const saSuccessResponse2 = Object.assign({}, saSuccessResponse); saSuccessResponse2.accessToken = 'SA_ACCESS_TOKEN2'; saSuccessResponse2.expireTime = new Date( - 2 * ONE_HOUR_IN_SECS * 1000 + 2 * ONE_HOUR_IN_SECS * 1000, ).toISOString(); const scopes: nock.Scope[] = []; @@ -1547,7 +1547,7 @@ describe('BaseExternalAccountClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -1555,7 +1555,7 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); scopes.push( mockGenerateAccessToken({ @@ -1563,7 +1563,7 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse2, token: stsSuccessfulResponse2.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); // Override 5min threshold with 10 second threshold. @@ -1627,10 +1627,10 @@ describe('BaseExternalAccountClient', () => { ], { authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds + basicAuthCreds, )}`, - } - ) + }, + ), ); scopes.push( mockGenerateAccessToken({ @@ -1638,11 +1638,11 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new TestExternalAccountClient( - externalAccountOptionsWithCredsAndSA + externalAccountOptionsWithCredsAndSA, ); const actualResponse = await client.getAccessToken(); @@ -1677,7 +1677,7 @@ describe('BaseExternalAccountClient', () => { }), }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -1685,11 +1685,11 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new TestExternalAccountClient( - externalAccountOptionsWithWorkforceUserProjectAndSA + externalAccountOptionsWithWorkforceUserProjectAndSA, ); const actualResponse = await client.getAccessToken(); @@ -1719,7 +1719,7 @@ describe('BaseExternalAccountClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -1728,7 +1728,7 @@ describe('BaseExternalAccountClient', () => { token: stsSuccessfulResponse.access_token, lifetime: 2800, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const externalAccountOptionsWithSATokenLifespan = Object.assign( @@ -1737,11 +1737,11 @@ describe('BaseExternalAccountClient', () => { token_lifetime_seconds: 2800, }, }, - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); const client = new TestExternalAccountClient( - externalAccountOptionsWithSATokenLifespan + externalAccountOptionsWithSATokenLifespan, ); const actualResponse = await client.getAccessToken(); @@ -1777,10 +1777,10 @@ describe('BaseExternalAccountClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'test', true, - false + false, ), - } - ) + }, + ), ); scopes.push( mockGenerateAccessToken({ @@ -1788,11 +1788,11 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new TestExternalAccountClient( - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); const actualResponse = await client.getAccessToken(); @@ -1828,10 +1828,10 @@ describe('BaseExternalAccountClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'test', true, - true + true, ), - } - ) + }, + ), ); scopes.push( mockGenerateAccessToken({ @@ -1840,7 +1840,7 @@ describe('BaseExternalAccountClient', () => { token: stsSuccessfulResponse.access_token, lifetime: 2800, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const externalAccountOptionsWithSATokenLifespan = Object.assign( @@ -1849,11 +1849,11 @@ describe('BaseExternalAccountClient', () => { token_lifetime_seconds: 2800, }, }, - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); const client = new TestExternalAccountClient( - externalAccountOptionsWithSATokenLifespan + externalAccountOptionsWithSATokenLifespan, ); const actualResponse = await client.getAccessToken(); @@ -1921,7 +1921,7 @@ describe('BaseExternalAccountClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -1929,11 +1929,11 @@ describe('BaseExternalAccountClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new TestExternalAccountClient( - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); const actualHeaders = await client.getRequestHeaders(); @@ -1965,7 +1965,7 @@ describe('BaseExternalAccountClient', () => { const optionsWithQuotaProjectId = Object.assign( {quota_project_id: quotaProjectId}, - externalAccountOptions + externalAccountOptions, ); const client = new TestExternalAccountClient(optionsWithQuotaProjectId); const actualHeaders = await client.getRequestHeaders(); @@ -1999,7 +1999,7 @@ describe('BaseExternalAccountClient', () => { const client = new TestExternalAccountClient(externalAccountOptions); await assert.rejects( client.getRequestHeaders(), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); scope.done(); }); @@ -2014,7 +2014,7 @@ describe('BaseExternalAccountClient', () => { }; const optionsWithQuotaProjectId = Object.assign( {quota_project_id: quotaProjectId}, - externalAccountOptions + externalAccountOptions, ); const exampleRequest = { key1: 'value1', @@ -2076,7 +2076,7 @@ describe('BaseExternalAccountClient', () => { }; const optionsWithQuotaProjectId = Object.assign( {quota_project_id: quotaProjectId}, - externalAccountOptionsWithSA + externalAccountOptionsWithSA, ); const exampleRequest = { key1: 'value1', @@ -2139,7 +2139,7 @@ describe('BaseExternalAccountClient', () => { }; const optionsWithQuotaProjectId = Object.assign( {quota_project_id: quotaProjectId}, - externalAccountOptions + externalAccountOptions, ); const exampleRequest = { key1: 'value1', @@ -2217,7 +2217,7 @@ describe('BaseExternalAccountClient', () => { method: 'POST', data: exampleRequest, }), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); scope.done(); }); @@ -2274,7 +2274,7 @@ describe('BaseExternalAccountClient', () => { assert.deepStrictEqual(result?.data, exampleResponse); scopes.forEach(scope => scope.done()); done(); - } + }, ); }); @@ -2328,7 +2328,7 @@ describe('BaseExternalAccountClient', () => { scopes.forEach(scope => scope.done()); done(); - } + }, ); }); @@ -2454,7 +2454,7 @@ describe('BaseExternalAccountClient', () => { }), { status: 401, - } + }, ); scopes.forEach(scope => scope.done()); @@ -2531,7 +2531,7 @@ describe('BaseExternalAccountClient', () => { }), { status: 403, - } + }, ); scopes.forEach(scope => scope.done()); @@ -2574,7 +2574,7 @@ describe('BaseExternalAccountClient', () => { const refreshedTokenResponse = await client.getAccessToken(); assert.deepStrictEqual( refreshedTokenResponse.token, - stsSuccessfulResponse.access_token + stsSuccessfulResponse.access_token, ); scope.done(); @@ -2596,7 +2596,7 @@ describe('BaseExternalAccountClient', () => { const unexpiredTokenResponse = await client.getAccessToken(); assert.deepStrictEqual( unexpiredTokenResponse.token, - credentials.access_token + credentials.access_token, ); }); }); diff --git a/test/test.compute.ts b/test/test.compute.ts index 05d476ec..981eac6c 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -57,7 +57,7 @@ describe('compute', () => { const compute = new Compute(); assert.strictEqual( 'compute-placeholder', - compute.credentials.refresh_token + compute.credentials.refresh_token, ); }); @@ -130,7 +130,7 @@ describe('compute', () => { await compute.request({url}); assert.strictEqual( compute.credentials.access_token, - 'initial-access-token' + 'initial-access-token', ); scope.done(); }); @@ -142,7 +142,7 @@ describe('compute', () => { await compute.request({url}); assert.strictEqual( compute.credentials.access_token, - 'initial-access-token' + 'initial-access-token', ); scope.done(); }); @@ -153,7 +153,7 @@ describe('compute', () => { 'A Forbidden error was returned while attempting to retrieve an access ' + 'token for the Compute Engine built-in service account. This may be because the ' + 'Compute Engine instance does not have the correct permission scopes specified. ' + - 'Could not refresh access token.' + 'Could not refresh access token.', ); await assert.rejects(compute.request({url}), expected); scope.done(); @@ -164,7 +164,7 @@ describe('compute', () => { const expected = new RegExp( 'A Not Found error was returned while attempting to retrieve an access' + 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have any permission scopes specified.' + 'Compute Engine instance does not have any permission scopes specified.', ); await assert.rejects(compute.request({url}), expected); scope.done(); @@ -183,7 +183,7 @@ describe('compute', () => { 'A Forbidden error was returned while attempting to retrieve an access ' + 'token for the Compute Engine built-in service account. This may be because the ' + 'Compute Engine instance does not have the correct permission scopes specified. ' + - 'Could not refresh access token.' + 'Could not refresh access token.', ); await assert.rejects(compute.request({}), expected); scope.done(); @@ -204,7 +204,7 @@ describe('compute', () => { 'A Not Found error was returned while attempting to retrieve an access' + 'token for the Compute Engine built-in service account. This may be because the ' + 'Compute Engine instance does not have any permission scopes specified. Could not ' + - 'refresh access token.' + 'refresh access token.', ); await assert.rejects(compute.request({}), expected); @@ -218,7 +218,7 @@ describe('compute', () => { mockExample(), nock(HOST_ADDRESS) .get( - `${BASE_PATH}/instance/service-accounts/${serviceAccountEmail}/token` + `${BASE_PATH}/instance/service-accounts/${serviceAccountEmail}/token`, ) .reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS), ]; diff --git a/test/test.crypto.ts b/test/test.crypto.ts index 3488c732..f9707f9a 100644 --- a/test/test.crypto.ts +++ b/test/test.crypto.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as fs from 'fs'; -import {assert} from 'chai'; +import {strict as assert} from 'assert'; import {describe, it} from 'mocha'; import {createCrypto, fromArrayBufferToHex} from '../src/crypto/crypto'; import {NodeCrypto} from '../src/crypto/node/crypto'; @@ -121,8 +121,8 @@ describe('crypto', () => { 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'; const extectedHash = new Uint8Array( (expectedHexHash.match(/.{1,2}/g) as string[]).map(byte => - parseInt(byte, 16) - ) + parseInt(byte, 16), + ), ); const calculatedHash = await crypto.signWithHmacSha256(key, message); @@ -136,8 +136,8 @@ describe('crypto', () => { 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'; const extectedHash = new Uint8Array( (expectedHexHash.match(/.{1,2}/g) as string[]).map(byte => - parseInt(byte, 16) - ) + parseInt(byte, 16), + ), ); const calculatedHash = await crypto.signWithHmacSha256(key, message); diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index 43e0717f..1cd259a9 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -120,7 +120,7 @@ describe('DownscopedClient', () => { describe('Constructor', () => { it('should throw on empty access boundary rule', () => { const expectedError = new Error( - 'At least one access boundary rule needs to be defined.' + 'At least one access boundary rule needs to be defined.', ); const cabWithEmptyAccessBoundaryRules = { accessBoundary: { @@ -138,7 +138,7 @@ describe('DownscopedClient', () => { it('should throw when number of access boundary rules is exceeded', () => { const expectedError = new Error( 'The provided access boundary has more than ' + - `${MAX_ACCESS_BOUNDARY_RULES_COUNT} access boundary rules.` + `${MAX_ACCESS_BOUNDARY_RULES_COUNT} access boundary rules.`, ); const cabWithExceedingAccessBoundaryRules: CredentialAccessBoundary = { accessBoundary: { @@ -154,20 +154,20 @@ describe('DownscopedClient', () => { }; for (let num = 0; num <= MAX_ACCESS_BOUNDARY_RULES_COUNT; num++) { cabWithExceedingAccessBoundaryRules.accessBoundary.accessBoundaryRules.push( - testAccessBoundaryRule + testAccessBoundaryRule, ); } assert.throws(() => { return new DownscopedClient( client, - cabWithExceedingAccessBoundaryRules + cabWithExceedingAccessBoundaryRules, ); }, expectedError); }); it('should throw on no permissions are defined in access boundary rules', () => { const expectedError = new Error( - 'At least one permission should be defined in access boundary rules.' + 'At least one permission should be defined in access boundary rules.', ); const cabWithNoPermissionIncluded = { accessBoundary: { @@ -266,7 +266,7 @@ describe('DownscopedClient', () => { assert.doesNotThrow(() => { return new DownscopedClient( client, - cabWithOnlyAvailabilityConditionExpression + cabWithOnlyAvailabilityConditionExpression, ); }); }); @@ -289,7 +289,7 @@ describe('DownscopedClient', () => { assert.doesNotThrow(() => { const instance = new DownscopedClient( client, - cabWithOneAccessBoundaryRule + cabWithOneAccessBoundaryRule, ); instance.quotaProjectId = quotaProjectId; return instance; @@ -316,7 +316,7 @@ describe('DownscopedClient', () => { }; const downscopedClient = new DownscopedClient( client, - cabWithOneAccessBoundaryRules + cabWithOneAccessBoundaryRules, ); downscopedClient.eagerRefreshThresholdMillis = refreshOptions.eagerRefreshThresholdMillis; @@ -324,11 +324,11 @@ describe('DownscopedClient', () => { refreshOptions.forceRefreshOnFailure; assert.strictEqual( downscopedClient.forceRefreshOnFailure, - refreshOptions.forceRefreshOnFailure + refreshOptions.forceRefreshOnFailure, ); assert.strictEqual( downscopedClient.eagerRefreshThresholdMillis, - refreshOptions.eagerRefreshThresholdMillis + refreshOptions.eagerRefreshThresholdMillis, ); }); }); @@ -340,11 +340,11 @@ describe('DownscopedClient', () => { }; const expectedError = new Error( 'The access token expiry_date field is missing in the provided ' + - 'credentials.' + 'credentials.', ); const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); assert.throws(() => { downscopedClient.setCredentials(credentials); @@ -359,7 +359,7 @@ describe('DownscopedClient', () => { }; const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); assert.doesNotThrow(() => { downscopedClient.setCredentials(credentials); @@ -368,7 +368,7 @@ describe('DownscopedClient', () => { assert.deepStrictEqual(tokenResponse.token, credentials.access_token); assert.deepStrictEqual( tokenResponse.expirationTime, - credentials.expiry_date + credentials.expiry_date, ); }); }); @@ -383,33 +383,33 @@ describe('DownscopedClient', () => { }; const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); downscopedClient.setCredentials(credentials); const tokenResponse = await downscopedClient.getAccessToken(); assert.deepStrictEqual(tokenResponse.token, credentials.access_token); assert.deepStrictEqual( tokenResponse.expirationTime, - credentials.expiry_date + credentials.expiry_date, ); assert.deepStrictEqual( tokenResponse.token, - downscopedClient.credentials.access_token + downscopedClient.credentials.access_token, ); assert.deepStrictEqual( tokenResponse.expirationTime, - downscopedClient.credentials.expiry_date + downscopedClient.credentials.expiry_date, ); clock.tick(ONE_HOUR_IN_SECS * 1000 - EXPIRATION_TIME_OFFSET - 1); const cachedTokenResponse = await downscopedClient.getAccessToken(); assert.deepStrictEqual( cachedTokenResponse.token, - credentials.access_token + credentials.access_token, ); assert.deepStrictEqual( cachedTokenResponse.expirationTime, - credentials.expiry_date + credentials.expiry_date, ); }); @@ -438,7 +438,7 @@ describe('DownscopedClient', () => { const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); // Listen to tokens events. On every event, push to list of // emittedEvents. @@ -476,19 +476,19 @@ describe('DownscopedClient', () => { assert.deepStrictEqual( refreshedTokenResponse.token, - stsSuccessfulResponse.access_token + stsSuccessfulResponse.access_token, ); assert.deepStrictEqual( refreshedTokenResponse.expirationTime, - expectedExpirationTime + expectedExpirationTime, ); assert.deepStrictEqual( refreshedTokenResponse.token, - downscopedClient.credentials.access_token + downscopedClient.credentials.access_token, ); assert.deepStrictEqual( refreshedTokenResponse.expirationTime, - downscopedClient.credentials.expiry_date + downscopedClient.credentials.expiry_date, ); scope.done(); }); @@ -510,21 +510,21 @@ describe('DownscopedClient', () => { ]); const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); assert.deepStrictEqual(downscopedClient.credentials, {}); const tokenResponse = await downscopedClient.getAccessToken(); assert.deepStrictEqual( tokenResponse.token, - stsSuccessfulResponse.access_token + stsSuccessfulResponse.access_token, ); assert.deepStrictEqual( tokenResponse.token, - downscopedClient.credentials.access_token + downscopedClient.credentials.access_token, ); assert.deepStrictEqual( tokenResponse.expirationTime, - downscopedClient.credentials.expiry_date + downscopedClient.credentials.expiry_date, ); scope.done(); }); @@ -564,12 +564,12 @@ describe('DownscopedClient', () => { const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); assert.deepStrictEqual(downscopedClient.credentials, {}); await assert.rejects( downscopedClient.getAccessToken(), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); assert.deepStrictEqual(downscopedClient.credentials, {}); // Next try should succeed. @@ -577,15 +577,15 @@ describe('DownscopedClient', () => { delete actualResponse.res; assert.deepStrictEqual( actualResponse.token, - stsSuccessfulResponse.access_token + stsSuccessfulResponse.access_token, ); assert.deepStrictEqual( actualResponse.token, - downscopedClient.credentials.access_token + downscopedClient.credentials.access_token, ); assert.deepStrictEqual( actualResponse.expirationTime, - downscopedClient.credentials.expiry_date + downscopedClient.credentials.expiry_date, ); scope.done(); }); @@ -596,7 +596,7 @@ describe('DownscopedClient', () => { const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); await assert.rejects(downscopedClient.getAccessToken(), expectedError); }); @@ -606,7 +606,7 @@ describe('DownscopedClient', () => { const expireDate = now + ONE_HOUR_IN_SECS * 1000; const stsSuccessfulResponseWithoutExpireInField = Object.assign( {}, - stsSuccessfulResponse + stsSuccessfulResponse, ); const emittedEvents: Credentials[] = []; delete stsSuccessfulResponseWithoutExpireInField.expires_in; @@ -628,7 +628,7 @@ describe('DownscopedClient', () => { client.expirationTime = expireDate; const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); // Listen to tokens events. On every event, push to list of // emittedEvents. @@ -650,12 +650,12 @@ describe('DownscopedClient', () => { assert.deepStrictEqual(tokenResponse.expirationTime, expireDate); assert.deepStrictEqual( tokenResponse.token, - stsSuccessfulResponseWithoutExpireInField.access_token + stsSuccessfulResponseWithoutExpireInField.access_token, ); assert.strictEqual(downscopedClient.credentials.expiry_date, expireDate); assert.strictEqual( downscopedClient.credentials.access_token, - stsSuccessfulResponseWithoutExpireInField.access_token + stsSuccessfulResponseWithoutExpireInField.access_token, ); scope.done(); }); @@ -663,7 +663,7 @@ describe('DownscopedClient', () => { it('should have no expiry date if source cred has no expiry time and STS response does not return one', async () => { const stsSuccessfulResponseWithoutExpireInField = Object.assign( {}, - stsSuccessfulResponse + stsSuccessfulResponse, ); const emittedEvents: Credentials[] = []; delete stsSuccessfulResponseWithoutExpireInField.expires_in; @@ -684,7 +684,7 @@ describe('DownscopedClient', () => { const downscopedClient = new DownscopedClient( client, - testClientAccessBoundary + testClientAccessBoundary, ); // Listen to tokens events. On every event, push to list of // emittedEvents. @@ -705,7 +705,7 @@ describe('DownscopedClient', () => { }); assert.deepStrictEqual( tokenResponse.token, - stsSuccessfulResponseWithoutExpireInField.access_token + stsSuccessfulResponseWithoutExpireInField.access_token, ); assert.deepStrictEqual(tokenResponse.expirationTime, null); assert.deepStrictEqual(downscopedClient.credentials.expiry_date, null); @@ -793,7 +793,7 @@ describe('DownscopedClient', () => { const cabClient = new DownscopedClient(client, testClientAccessBoundary); await assert.rejects( cabClient.getRequestHeaders(), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); scope.done(); }); @@ -936,7 +936,7 @@ describe('DownscopedClient', () => { method: 'POST', data: exampleRequest, }), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); scope.done(); }); @@ -993,7 +993,7 @@ describe('DownscopedClient', () => { assert.deepStrictEqual(result?.data, exampleResponse); scopes.forEach(scope => scope.done()); done(); - } + }, ); }); @@ -1047,7 +1047,7 @@ describe('DownscopedClient', () => { scopes.forEach(scope => scope.done()); done(); - } + }, ); }); @@ -1172,7 +1172,7 @@ describe('DownscopedClient', () => { }), { status: 401, - } + }, ); scopes.forEach(scope => scope.done()); @@ -1247,7 +1247,7 @@ describe('DownscopedClient', () => { }), { status: 403, - } + }, ); scopes.forEach(scope => scope.done()); }); diff --git a/test/test.executableresponse.ts b/test/test.executableresponse.ts index 47705dc4..778aa75a 100644 --- a/test/test.executableresponse.ts +++ b/test/test.executableresponse.ts @@ -22,6 +22,7 @@ import { InvalidMessageFieldError, InvalidSuccessFieldError, InvalidVersionFieldError, + ExecutableResponseJson, } from '../src/auth/executable-response'; import * as sinon from 'sinon'; @@ -49,13 +50,13 @@ describe('ExecutableResponse', () => { success: 'true', }; const expectedError = new InvalidVersionFieldError( - "Executable response must contain a 'version' field." + "Executable response must contain a 'version' field.", ); assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new ExecutableResponse(responseJson); + return new ExecutableResponse( + responseJson as unknown as ExecutableResponseJson, + ); }, expectedError); }); @@ -64,13 +65,13 @@ describe('ExecutableResponse', () => { version: 1, }; const expectedError = new InvalidSuccessFieldError( - "Executable response must contain a 'success' field." + "Executable response must contain a 'success' field.", ); assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new ExecutableResponse(responseJson); + return new ExecutableResponse( + responseJson as unknown as ExecutableResponseJson, + ); }, expectedError); }); @@ -82,7 +83,7 @@ describe('ExecutableResponse', () => { }; const expectedError = new InvalidTokenTypeFieldError( "Executable response must contain a 'token_type' field when successful " + - `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.` + `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.`, ); assert.throws(() => { @@ -99,7 +100,7 @@ describe('ExecutableResponse', () => { }; const expectedError = new InvalidTokenTypeFieldError( "Executable response must contain a 'token_type' field when successful " + - `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.` + `and it must be one of ${OIDC_SUBJECT_TOKEN_TYPE1}, ${OIDC_SUBJECT_TOKEN_TYPE2}, or ${SAML_SUBJECT_TOKEN_TYPE}.`, ); assert.throws(() => { @@ -116,7 +117,7 @@ describe('ExecutableResponse', () => { saml_response: 'response', }; const expectedError = new InvalidSubjectTokenError( - "Executable response must contain a 'id_token' field when token_type=urn:ietf:params:oauth:token-type:id_token or urn:ietf:params:oauth:token-type:jwt." + "Executable response must contain a 'id_token' field when token_type=urn:ietf:params:oauth:token-type:id_token or urn:ietf:params:oauth:token-type:jwt.", ); assert.throws(() => { @@ -133,7 +134,7 @@ describe('ExecutableResponse', () => { id_token: 'response', }; const expectedError = new InvalidSubjectTokenError( - "Executable response must contain a 'saml_response' field when token_type=urn:ietf:params:oauth:token-type:saml2." + "Executable response must contain a 'saml_response' field when token_type=urn:ietf:params:oauth:token-type:saml2.", ); assert.throws(() => { @@ -148,7 +149,7 @@ describe('ExecutableResponse', () => { message: 'error message', }; const expectedError = new InvalidCodeFieldError( - "Executable response must contain a 'code' field when unsuccessful." + "Executable response must contain a 'code' field when unsuccessful.", ); assert.throws(() => { @@ -163,7 +164,7 @@ describe('ExecutableResponse', () => { code: '1', }; const expectedError = new InvalidMessageFieldError( - "Executable response must contain a 'message' field when unsuccessful." + "Executable response must contain a 'message' field when unsuccessful.", ); assert.throws(() => { @@ -187,7 +188,7 @@ describe('ExecutableResponse', () => { assert.equal(executableResponse.tokenType, responseJson.token_type); assert.equal( executableResponse.expirationTime, - responseJson.expiration_time + responseJson.expiration_time, ); assert.equal(executableResponse.subjectToken, responseJson.saml_response); }); @@ -208,7 +209,7 @@ describe('ExecutableResponse', () => { assert.equal(executableResponse.tokenType, responseJson.token_type); assert.equal( executableResponse.expirationTime, - responseJson.expiration_time + responseJson.expiration_time, ); assert.equal(executableResponse.subjectToken, responseJson.id_token); }); @@ -229,7 +230,7 @@ describe('ExecutableResponse', () => { assert.equal(executableResponse.tokenType, responseJson.token_type); assert.equal( executableResponse.expirationTime, - responseJson.expiration_time + responseJson.expiration_time, ); assert.equal(executableResponse.subjectToken, responseJson.id_token); }); diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index f13de319..89f23c21 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -58,13 +58,13 @@ describe('ExternalAccountAuthorizedUserClient', () => { url: string, path: string, nockParams: NockMockRefreshResponse[], - additionalHeaders?: {[key: string]: string} + additionalHeaders?: {[key: string]: string}, ): nock.Scope { const headers = Object.assign( { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, - additionalHeaders || {} + additionalHeaders || {}, ); const scope = nock(url, { reqheaders: headers, @@ -125,14 +125,14 @@ describe('ExternalAccountAuthorizedUserClient', () => { it('should not throw when valid options are provided', () => { assert.doesNotThrow(() => { return new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); }); }); it('should set default RefreshOptions', () => { const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); assert(!client.forceRefreshOnFailure); @@ -152,7 +152,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptionsNoToken + externalAccountAuthorizedUserCredentialOptionsNoToken, ); await client.getAccessToken(); scope.done(); @@ -189,18 +189,18 @@ describe('ExternalAccountAuthorizedUserClient', () => { assert.strictEqual( client.forceRefreshOnFailure, - refreshOptions.forceRefreshOnFailure + refreshOptions.forceRefreshOnFailure, ); assert.strictEqual( client.eagerRefreshThresholdMillis, - refreshOptions.eagerRefreshThresholdMillis + refreshOptions.eagerRefreshThresholdMillis, ); }); describe('universeDomain', () => { it('should be the default universe if not set', () => { const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); assert.equal(client.universeDomain, DEFAULT_UNIVERSE); @@ -232,7 +232,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); const actualResponse = await client.getAccessToken(); assertGaxiosResponsePresent(actualResponse); @@ -262,11 +262,11 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); await assert.rejects( client.getAccessToken(), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); scope.done(); }); @@ -291,7 +291,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { .reply(200, successfulRefreshResponse); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); const actualResponse = await client.getAccessToken(); @@ -325,7 +325,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); // Get initial access token and new refresh token. await client.getAccessToken(); @@ -355,7 +355,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); // Get initial access token and new refresh token. await client.getAccessToken(); @@ -363,7 +363,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { clock.tick( successfulRefreshResponseNoRefreshToken.expires_in * 1000 - client.eagerRefreshThresholdMillis - - 1 + 1, ); // Refresh access token with new access token. const actualResponse = await client.getAccessToken(); @@ -390,7 +390,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); // Get initial access token. await client.getAccessToken(); @@ -398,7 +398,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { clock.tick( successfulRefreshResponseNoRefreshToken.expires_in * 1000 - client.eagerRefreshThresholdMillis + - 1 + 1, ); // Refresh access token with new access token. const actualResponse = await client.getAccessToken(); @@ -431,10 +431,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { const optionsWithQuotaProjectId = Object.assign( {quota_project_id: 'quotaProjectId'}, - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); const client = new ExternalAccountAuthorizedUserClient( - optionsWithQuotaProjectId + optionsWithQuotaProjectId, ); const actualHeaders = await client.getRequestHeaders(); @@ -460,11 +460,11 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); await assert.rejects( client.getRequestHeaders(), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); scope.done(); }); @@ -479,7 +479,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { }; const optionsWithQuotaProjectId = Object.assign( {quota_project_id: quotaProjectId}, - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); const exampleRequest = { key1: 'value1', @@ -512,7 +512,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]; const client = new ExternalAccountAuthorizedUserClient( - optionsWithQuotaProjectId + optionsWithQuotaProjectId, ); const actualResponse = await client.request({ url: 'https://example.com/api', @@ -547,7 +547,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]); const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); await assert.rejects( client.request({ @@ -555,7 +555,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { method: 'POST', data: exampleRequest, }), - getErrorFromOAuthErrorResponse(errorResponse) + getErrorFromOAuthErrorResponse(errorResponse), ); scope.done(); }); @@ -595,7 +595,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]; const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); client.request( { @@ -609,7 +609,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { assert.deepStrictEqual(result?.data, exampleResponse); scopes.forEach(scope => scope.done()); done(); - } + }, ); }); @@ -645,7 +645,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]; const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); client.request( { @@ -660,7 +660,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { scopes.forEach(scope => scope.done()); done(); - } + }, ); }); @@ -762,7 +762,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { }), { status: 401, - } + }, ); scopes.forEach(scope => scope.done()); @@ -817,7 +817,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { }), { status: 403, - } + }, ); scopes.forEach(scope => scope.done()); }); @@ -853,7 +853,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { ]; const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions + externalAccountAuthorizedUserCredentialOptions, ); // Send request with no headers. const actualResponse = await client.request({ diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index 54a89d53..e573a1c6 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -113,7 +113,7 @@ describe('ExternalAccountClient', () => { assert.deepStrictEqual( ExternalAccountClient.fromJSON(fileSourcedOptions), - expectedClient + expectedClient, ); }); @@ -128,7 +128,7 @@ describe('ExternalAccountClient', () => { ...fileSourcedOptions, ...refreshOptions, }), - expectedClient + expectedClient, ); }); @@ -137,7 +137,7 @@ describe('ExternalAccountClient', () => { assert.deepStrictEqual( ExternalAccountClient.fromJSON(awsOptions), - expectedClient + expectedClient, ); }); @@ -146,7 +146,7 @@ describe('ExternalAccountClient', () => { assert.deepStrictEqual( ExternalAccountClient.fromJSON({...awsOptions, ...refreshOptions}), - expectedClient + expectedClient, ); }); @@ -162,30 +162,30 @@ describe('ExternalAccountClient', () => { { workforce_pool_user_project: 'workforce_pool_user_project', subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - } + }, ); for (const validWorkforceIdentityPoolClientAudience of validWorkforceIdentityPoolClientAudiences) { workforceFileSourcedOptions.audience = validWorkforceIdentityPoolClientAudience; const expectedClient = new IdentityPoolClient( - workforceFileSourcedOptions + workforceFileSourcedOptions, ); assert.deepStrictEqual( ExternalAccountClient.fromJSON(workforceFileSourcedOptions), - expectedClient + expectedClient, ); } }); it('should return PluggableAuthClient on PluggableAuthClientOptions', () => { const expectedClient = new PluggableAuthClient( - pluggableAuthClientOptions + pluggableAuthClientOptions, ); assert.deepStrictEqual( ExternalAccountClient.fromJSON(pluggableAuthClientOptions), - expectedClient + expectedClient, ); }); @@ -200,7 +200,7 @@ describe('ExternalAccountClient', () => { ...pluggableAuthClientOptions, ...refreshOptions, }), - expectedClient + expectedClient, ); }); @@ -212,7 +212,7 @@ describe('ExternalAccountClient', () => { { workforce_pool_user_project: 'workforce_pool_user_project', subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - } + }, ); it(`should throw an error when an invalid workforce audience ${invalidWorkforceIdentityPoolClientAudience} is provided with a workforce user project`, () => { workforceIdentityPoolClientInvalidOptions.audience = @@ -220,17 +220,17 @@ describe('ExternalAccountClient', () => { assert.throws(() => { return ExternalAccountClient.fromJSON( - workforceIdentityPoolClientInvalidOptions + workforceIdentityPoolClientInvalidOptions, ); }); }); - } + }, ); it('should return null when given non-ExternalAccountClientOptions', () => { assert( // eslint-disable-next-line @typescript-eslint/no-explicit-any - ExternalAccountClient.fromJSON(serviceAccountKeys as any) === null + ExternalAccountClient.fromJSON(serviceAccountKeys as any) === null, ); }); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 73537d91..60a0877c 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -100,7 +100,7 @@ describe('googleauth', () => { 'fake', 'home', 'gcloud', - 'application_default_credentials.json' + 'application_default_credentials.json', ); const wellKnownPathLinux = path.join( '/', @@ -108,7 +108,7 @@ describe('googleauth', () => { 'user', '.config', 'gcloud', - 'application_default_credentials.json' + 'application_default_credentials.json', ); function createGTokenMock(body: CredentialRequest) { return nock('https://oauth2.googleapis.com') @@ -202,7 +202,7 @@ describe('googleauth', () => { } function mockLinuxWellKnownFile( - filePath = './test/fixtures/private2.json' + filePath = './test/fixtures/private2.json', ) { exposeLinuxWellKnownFile = true; createLinuxWellKnownStream = () => fs.createReadStream(filePath); @@ -320,7 +320,7 @@ describe('googleauth', () => { await auth.getRequestHeaders(), new Headers({ 'X-Goog-Api-Key': apiKey, - }) + }), ); }); @@ -333,7 +333,7 @@ describe('googleauth', () => { // API key should supported via `clientOptions` clientOptions: {apiKey}, }), - new RangeError(GoogleAuthExceptionMessages.API_KEY_WITH_CREDENTIALS) + new RangeError(GoogleAuthExceptionMessages.API_KEY_WITH_CREDENTIALS), ); }); @@ -574,13 +574,13 @@ describe('googleauth', () => { return; } await auth._getApplicationCredentialsFromFilePath( - './test/fixtures/goodlink' + './test/fixtures/goodlink', ); }); it('getApplicationCredentialsFromFilePath should error on invalid symlink', async () => { await assert.rejects( - auth._getApplicationCredentialsFromFilePath('./test/fixtures/badlink') + auth._getApplicationCredentialsFromFilePath('./test/fixtures/badlink'), ); }); @@ -590,7 +590,9 @@ describe('googleauth', () => { return; } await assert.rejects( - auth._getApplicationCredentialsFromFilePath('./test/fixtures/emptylink') + auth._getApplicationCredentialsFromFilePath( + './test/fixtures/emptylink', + ), ); }); @@ -628,7 +630,7 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should error on invalid file path', async () => { try { await auth._getApplicationCredentialsFromFilePath( - './nonexistantfile.json' + './nonexistantfile.json', ); } catch (e) { return; @@ -640,14 +642,14 @@ describe('googleauth', () => { // Make sure that the following path actually does point to a directory. const directory = './test/fixtures'; await assert.rejects( - auth._getApplicationCredentialsFromFilePath(directory) + auth._getApplicationCredentialsFromFilePath(directory), ); }); it('getApplicationCredentialsFromFilePath should handle errors thrown from createReadStream', async () => { await assert.rejects( auth._getApplicationCredentialsFromFilePath('./does/not/exist.json'), - /ENOENT: no such file or directory/ + /ENOENT: no such file or directory/, ); }); @@ -655,9 +657,9 @@ describe('googleauth', () => { sandbox.stub(auth, 'fromStream').throws('🤮'); await assert.rejects( auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json' + './test/fixtures/private.json', ), - /🤮/ + /🤮/, ); }); @@ -666,15 +668,15 @@ describe('googleauth', () => { sandbox.stub(auth, 'fromStream').throws('🤮'); await assert.rejects( auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json' + './test/fixtures/private.json', ), - /🤮/ + /🤮/, ); }); it('getApplicationCredentialsFromFilePath should correctly read the file and create a valid JWT', async () => { const result = await auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json' + './test/fixtures/private.json', ); assert(result); const jwt = result as JWT; @@ -688,7 +690,7 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should correctly read the file and create a valid JWT with eager refresh', async () => { const result = await auth._getApplicationCredentialsFromFilePath( './test/fixtures/private.json', - {eagerRefreshThresholdMillis: 7000} + {eagerRefreshThresholdMillis: 7000}, ); assert(result); const jwt = result as JWT; @@ -731,7 +733,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' + './test/fixtures/private.json', ); const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); @@ -747,7 +749,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' + './test/fixtures/private.json', ); const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable({ @@ -816,7 +818,7 @@ describe('googleauth', () => { .rejects('🤮'); await assert.rejects( auth._tryGetApplicationCredentialsFromWellKnownFile(), - /🤮/ + /🤮/, ); }); @@ -827,7 +829,7 @@ describe('googleauth', () => { .rejects('🤮'); await assert.rejects( auth._tryGetApplicationCredentialsFromWellKnownFile(), - /🤮/ + /🤮/, ); }); @@ -913,7 +915,7 @@ describe('googleauth', () => { mockEnvVar('GOOGLE_CLOUD_PROJECT', STUB_PROJECT); mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' + './test/fixtures/private2.json', ); const PROJECT_ID = 'configured-project-id-should-be-preferred'; const auth = new GoogleAuth({projectId: PROJECT_ID}); @@ -954,7 +956,7 @@ describe('googleauth', () => { // on an environment variable json file, but not on anything else. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' + './test/fixtures/private2.json', ); // Ask for credentials, the first time. @@ -1029,7 +1031,7 @@ describe('googleauth', () => { // * Running on GCE is set to true. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' + './test/fixtures/private2.json', ); mockWindows(); mockWindowsWellKnownFile(); @@ -1061,7 +1063,7 @@ describe('googleauth', () => { it('explicitly set quota project should not be overriden by environment value', async () => { mockLinuxWellKnownFile( - './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json', ); mockEnvVar('GOOGLE_CLOUD_QUOTA_PROJECT', 'quota_from_env'); let result = await auth.getApplicationDefault(); @@ -1076,7 +1078,7 @@ describe('googleauth', () => { it('getApplicationDefault should use quota project id from file if environment variable is empty', async () => { mockLinuxWellKnownFile( - './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json', ); mockEnvVar('GOOGLE_CLOUD_QUOTA_PROJECT', ''); const result = await auth.getApplicationDefault(); @@ -1086,7 +1088,7 @@ describe('googleauth', () => { it('getApplicationDefault should use quota project id from file if environment variable is not set', async () => { mockLinuxWellKnownFile( - './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json', ); const result = await auth.getApplicationDefault(); const client = result.credential as JWT; @@ -1105,7 +1107,7 @@ describe('googleauth', () => { // a JWTClient. assert.strictEqual( 'compute-placeholder', - (res.credential as OAuth2Client).credentials.refresh_token + (res.credential as OAuth2Client).credentials.refresh_token, ); }); @@ -1128,7 +1130,7 @@ describe('googleauth', () => { // * Running on GCE is set to true. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' + './test/fixtures/private2.json', ); mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT); mockWindows(); @@ -1176,7 +1178,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' + './test/fixtures/private.json', ); const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); @@ -1192,7 +1194,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' + './test/fixtures/private.json', ); const spy = sinon.spy(auth, 'getClient'); @@ -1255,7 +1257,7 @@ describe('googleauth', () => { const auth = new GoogleAuth({keyFilename: './funky/fresh.json'}); await assert.rejects( auth.getClient(), - /ENOENT: no such file or directory/ + /ENOENT: no such file or directory/, ); }); @@ -1317,7 +1319,7 @@ describe('googleauth', () => { scopes.forEach(s => s.done()); assert.deepStrictEqual( headers, - new Headers({authorization: 'Bearer abc123'}) + new Headers({authorization: 'Bearer abc123'}), ); }); @@ -1328,7 +1330,7 @@ describe('googleauth', () => { scopes.forEach(s => s.done()); assert.deepStrictEqual( opts.headers, - new Headers({authorization: 'Bearer abc123'}) + new Headers({authorization: 'Bearer abc123'}), ); }); @@ -1353,7 +1355,7 @@ describe('googleauth', () => { it('should cache prior call to getEnv(), when GCE', async () => { envDetect.clear(); const {auth} = mockGCE(); - auth.getEnv(); + auth.getEnv().catch(console.error); const env = await auth.getEnv(); assert.strictEqual(env, envDetect.GCPEnv.COMPUTE_ENGINE); }); @@ -1364,7 +1366,7 @@ describe('googleauth', () => { const scope = nock(host) .get(`${instancePath}/attributes/cluster-name`) .reply(200, {}, HEADERS); - auth.getEnv(); + auth.getEnv().catch(console.error); const env = await auth.getEnv(); assert.strictEqual(env, envDetect.GCPEnv.KUBERNETES_ENGINE); scope.done(); @@ -1432,7 +1434,7 @@ describe('googleauth', () => { sinon .stub( auth as unknown as {getProjectIdAsync: () => Promise}, - 'getProjectIdAsync' + 'getProjectIdAsync', ) .resolves(); @@ -1445,7 +1447,7 @@ describe('googleauth', () => { const data = 'abc123'; scopes.push( nock(iamUri).post(iamPath).reply(200, {signedBlob}), - nock(host).get(svcAccountPath).reply(200, email, HEADERS) + nock(host).get(svcAccountPath).reply(200, email, HEADERS), ); const value = await auth.sign(data); scopes.forEach(x => x.done()); @@ -1469,26 +1471,26 @@ describe('googleauth', () => { sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); await assert.rejects( auth.getProjectId(), - /Unable to detect a Project Id in the current environment/ + /Unable to detect a Project Id in the current environment/, ); }); it('getRequestHeaders populates x-goog-user-project with quota_project if present', async () => { const tokenReq = mockApplicationDefaultCredentials( - './test/fixtures/config-with-quota' + './test/fixtures/config-with-quota', ); const auth = new GoogleAuth(); const headers = await auth.getRequestHeaders(); assert.strictEqual( headers.get('x-goog-user-project'), - 'my-quota-project' + 'my-quota-project', ); tokenReq.done(); }); it('getRequestHeaders does not populate x-goog-user-project if quota_project is not present', async () => { const tokenReq = mockApplicationDefaultCredentials( - './test/fixtures/config-no-quota' + './test/fixtures/config-no-quota', ); const auth = new GoogleAuth(); const headers = await auth.getRequestHeaders(); @@ -1498,7 +1500,7 @@ describe('googleauth', () => { it('getRequestHeaders populates x-goog-user-project when called on returned client', async () => { const tokenReq = mockApplicationDefaultCredentials( - './test/fixtures/config-with-quota' + './test/fixtures/config-with-quota', ); const auth = new GoogleAuth(); const client = await auth.getClient(); @@ -1506,14 +1508,14 @@ describe('googleauth', () => { const headers = await client.getRequestHeaders(); assert.strictEqual( headers.get('x-goog-user-project'), - 'my-quota-project' + 'my-quota-project', ); tokenReq.done(); }); it('populates x-goog-user-project when request is made', async () => { const tokenReq = mockApplicationDefaultCredentials( - './test/fixtures/config-with-quota' + './test/fixtures/config-with-quota', ); const auth = new GoogleAuth(); const client = await auth.getClient(); @@ -1523,7 +1525,7 @@ describe('googleauth', () => { .reply(function () { assert.strictEqual( this.req.headers['x-goog-user-project'], - 'my-quota-project' + 'my-quota-project', ); return [200, RESPONSE_BODY]; }); @@ -1550,7 +1552,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' + './test/fixtures/private.json', ); const auth = new GoogleAuth(); @@ -1563,7 +1565,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/refresh.json' + './test/fixtures/refresh.json', ); mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id'); @@ -1576,7 +1578,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/refresh.json' + './test/fixtures/refresh.json', ); mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id'); @@ -1586,7 +1588,7 @@ describe('googleauth', () => { // Setup variables const idTokenPayload = Buffer.from(JSON.stringify({exp: 100})).toString( - 'base64' + 'base64', ); const testIdToken = `TEST.${idTokenPayload}.TOKEN`; const targetAudience = 'a-target-audience'; @@ -1626,7 +1628,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json' + './test/fixtures/private.json', ); const spy = sinon.spy(auth, 'getClient'); @@ -1653,10 +1655,10 @@ describe('googleauth', () => { it('should get the universe from ADC', async () => { mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private2.json' + './test/fixtures/private2.json', ); const {universe_domain} = JSON.parse( - fs.readFileSync('./test/fixtures/private2.json', 'utf-8') + fs.readFileSync('./test/fixtures/private2.json', 'utf-8'), ); assert(universe_domain); @@ -1695,7 +1697,7 @@ describe('googleauth', () => { type: IMPERSONATED_ACCOUNT_TYPE, service_account_impersonation_url: new URL( './test@test-project.iam.gserviceaccount.com:generateAccessToken', - serviceAccountImpersonationURLBase + serviceAccountImpersonationURLBase, ).toString(), source_credentials: { client_id: 'client', @@ -1712,7 +1714,7 @@ describe('googleauth', () => { type: IMPERSONATED_ACCOUNT_TYPE, service_account_impersonation_url: new URL( './test@test-project.iam.gserviceaccount.com:generateIdToken', - serviceAccountImpersonationURLBase + serviceAccountImpersonationURLBase, ).toString(), source_credentials: { type: 'service_account', @@ -1728,7 +1730,7 @@ describe('googleauth', () => { type: IMPERSONATED_ACCOUNT_TYPE, service_account_impersonation_url: new URL( './test@test-project.iam.gserviceaccount.com:generateIdToken', - serviceAccountImpersonationURLBase + serviceAccountImpersonationURLBase, ).toString(), source_credentials: { type: EXTERNAL_ACCOUNT_TYPE, @@ -1751,7 +1753,7 @@ describe('googleauth', () => { // This is a private prop - we will refactor/remove in the future assert( (client as unknown as {sourceClient: {}}).sourceClient instanceof - expectedSource + expectedSource, ); } }); @@ -1768,7 +1770,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/impersonated_application_default_credentials.json' + './test/fixtures/impersonated_application_default_credentials.json', ); // Set up a mock to explicity return the Project ID, as needed for impersonated ADC @@ -1801,7 +1803,7 @@ describe('googleauth', () => { authorization: `Bearer ${saSuccessResponse.accessToken}`, 'content-type': 'application/json', }, - } + }, ) .reply(200, {keyId: keyId, signedBlob: signedBlob}), ]; @@ -1834,7 +1836,7 @@ describe('googleauth', () => { }; const fileSubjectToken = fs.readFileSync( externalAccountJSON.credential_source.file, - 'utf-8' + 'utf-8', ); // Project number should match the project number in externalAccountJSON. const projectNumber = '123456'; @@ -1863,7 +1865,7 @@ describe('googleauth', () => { function createExternalAccountJSON() { const credentialSourceCopy = Object.assign( {}, - externalAccountJSON.credential_source + externalAccountJSON.credential_source, ); const jsonCopy = Object.assign({}, externalAccountJSON); jsonCopy.credential_source = credentialSourceCopy; @@ -1884,7 +1886,7 @@ describe('googleauth', () => { function mockGetAccessTokenAndProjectId( mockProjectIdRetrieval = true, expectedScopes = ['https://www.googleapis.com/auth/cloud-platform'], - mockServiceAccountImpersonation = false + mockServiceAccountImpersonation = false, ): nock.Scope[] { const stsScopes = mockServiceAccountImpersonation ? 'https://www.googleapis.com/auth/cloud-platform' @@ -1913,7 +1915,7 @@ describe('googleauth', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: expectedScopes, - }) + }), ); } @@ -1923,8 +1925,8 @@ describe('googleauth', () => { projectNumber, stsSuccessfulResponse.access_token, 200, - projectInfoResponse - ) + projectInfoResponse, + ), ); } @@ -1940,7 +1942,7 @@ describe('googleauth', () => { */ function assertExternalAccountClientInitialized( actualClient: AuthClient, - json: ExternalAccountClientOptions + json: ExternalAccountClientOptions, ) { // Confirm expected client is initialized. assert(fromJsonSpy.calledOnceWithExactly(json)); @@ -1974,7 +1976,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized(result, json); assert.strictEqual( (result as BaseExternalAccountClient).scopes, - defaultScopes + defaultScopes, ); }); @@ -1987,7 +1989,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized(result, json); assert.strictEqual( (result as BaseExternalAccountClient).scopes, - userScopes + userScopes, ); }); @@ -2015,19 +2017,19 @@ describe('googleauth', () => { describe('fromStream()', () => { it('should read the stream and create a client', async () => { const stream = fs.createReadStream( - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); const actualClient = await auth.fromStream(stream); assertExternalAccountClientInitialized( actualClient, - createExternalAccountJSON() + createExternalAccountJSON(), ); }); it('should include provided RefreshOptions in client', async () => { const stream = fs.createReadStream( - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); const auth = new GoogleAuth(); const result = await auth.fromStream(stream, refreshOptions); @@ -2046,7 +2048,7 @@ describe('googleauth', () => { // external-account-cred.json mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); const res = await auth.getApplicationDefault(); @@ -2054,7 +2056,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON() + createExternalAccountJSON(), ); // Project ID should also be set. assert.deepEqual(client.projectId, projectId); @@ -2067,7 +2069,7 @@ describe('googleauth', () => { // external-account-cred.json mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); const auth = new GoogleAuth(); @@ -2077,11 +2079,11 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON() + createExternalAccountJSON(), ); assert.strictEqual( (client as BaseExternalAccountClient).scopes, - defaultScopes + defaultScopes, ); scopes.forEach(s => s.done()); }); @@ -2092,7 +2094,7 @@ describe('googleauth', () => { // external-account-cred.json mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); const auth = new GoogleAuth({scopes: userScopes}); @@ -2102,11 +2104,11 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON() + createExternalAccountJSON(), ); assert.strictEqual( (client as BaseExternalAccountClient).scopes, - userScopes + userScopes, ); scopes.forEach(s => s.done()); }); @@ -2123,7 +2125,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON() + createExternalAccountJSON(), ); assert.deepEqual(client.projectId, projectId); scopes.forEach(s => s.done()); @@ -2143,11 +2145,11 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON() + createExternalAccountJSON(), ); assert.strictEqual( (client as BaseExternalAccountClient).scopes, - defaultScopes + defaultScopes, ); scopes.forEach(s => s.done()); }); @@ -2166,11 +2168,11 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( client, - createExternalAccountJSON() + createExternalAccountJSON(), ); assert.strictEqual( (client as BaseExternalAccountClient).scopes, - userScopes + userScopes, ); scopes.forEach(s => s.done()); }); @@ -2179,7 +2181,7 @@ describe('googleauth', () => { // Environment variable is set up to point to external-account-cred.json mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); const auth = new GoogleAuth(); @@ -2189,7 +2191,7 @@ describe('googleauth', () => { auth as {} as { getProjectIdAsync: Promise; }, - 'getProjectIdAsync' + 'getProjectIdAsync', ) .resolves(null); @@ -2203,19 +2205,19 @@ describe('googleauth', () => { it('should correctly read the file and create a valid client', async () => { const actualClient = await auth._getApplicationCredentialsFromFilePath( - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); assertExternalAccountClientInitialized( actualClient, - createExternalAccountJSON() + createExternalAccountJSON(), ); }); it('should include provided RefreshOptions in client', async () => { const result = await auth._getApplicationCredentialsFromFilePath( './test/fixtures/external-account-cred.json', - refreshOptions + refreshOptions, ); assertExternalAccountClientInitialized(result, { @@ -2260,8 +2262,8 @@ describe('googleauth', () => { message: 'The caller does not have permission', status: 'PERMISSION_DENIED', }, - } - ) + }, + ), ); const keyFilename = './test/fixtures/external-account-cred.json'; const auth = new GoogleAuth({keyFilename}); @@ -2277,7 +2279,7 @@ describe('googleauth', () => { await assert.rejects( auth.getProjectId(), - /The file at invalid does not exist, or it is not a file/ + /The file at invalid does not exist, or it is not a file/, ); }); @@ -2288,7 +2290,7 @@ describe('googleauth', () => { await assert.rejects( auth.getProjectId(), - /Unable to detect a Project Id in the current environment/ + /Unable to detect a Project Id in the current environment/, ); }); }); @@ -2297,11 +2299,11 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); const result = await auth._tryGetApplicationCredentialsFromEnvironmentVariable( - refreshOptions + refreshOptions, ); assert(result); @@ -2316,7 +2318,7 @@ describe('googleauth', () => { mockLinuxWellKnownFile('./test/fixtures/external-account-cred.json'); const result = await auth._tryGetApplicationCredentialsFromWellKnownFile( - refreshOptions + refreshOptions, ); assert(result); @@ -2329,7 +2331,7 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath() should resolve', async () => { const result = await auth._getApplicationCredentialsFromFilePath( './test/fixtures/external-account-cred.json', - refreshOptions + refreshOptions, ); assertExternalAccountClientInitialized(result, { @@ -2347,7 +2349,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( actualClient, - createExternalAccountJSON() + createExternalAccountJSON(), ); }); @@ -2358,7 +2360,7 @@ describe('googleauth', () => { assertExternalAccountClientInitialized( actualClient, - createExternalAccountJSON() + createExternalAccountJSON(), ); }); @@ -2367,14 +2369,14 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-cred.json' + './test/fixtures/external-account-cred.json', ); const auth = new GoogleAuth(); const client = await auth.getClient(); assertExternalAccountClientInitialized( client, - createExternalAccountJSON() + createExternalAccountJSON(), ); scopes.forEach(s => s.done()); }); @@ -2383,7 +2385,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/impersonated_application_default_credentials.json' + './test/fixtures/impersonated_application_default_credentials.json', ); // Set up a mock to explicity return the Project ID, as needed for impersonated ADC @@ -2404,7 +2406,7 @@ describe('googleauth', () => { }), nock('https://iamcredentials.googleapis.com') .post( - '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken' + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', ) .reply(200, { accessToken: 'qwerty345', @@ -2466,7 +2468,7 @@ describe('googleauth', () => { await assert.rejects( auth.sign('abc123'), - /Cannot sign data without `client_email`/ + /Cannot sign data without `client_email`/, ); }); @@ -2474,7 +2476,7 @@ describe('googleauth', () => { const scopes = mockGetAccessTokenAndProjectId( false, ['https://www.googleapis.com/auth/cloud-platform'], - true + true, ); const email = saEmail; const configWithImpersonation = createExternalAccountJSON(); @@ -2496,9 +2498,9 @@ describe('googleauth', () => { authorization: `Bearer ${saSuccessResponse.accessToken}`, 'content-type': 'application/json', }, - } + }, ) - .reply(200, {signedBlob}) + .reply(200, {signedBlob}), ); const auth = new GoogleAuth({ credentials: configWithImpersonation, @@ -2520,7 +2522,7 @@ describe('googleauth', () => { await assert.rejects( auth.getIdTokenClient('a-target-audience'), - /Cannot fetch ID token in this environment/ + /Cannot fetch ID token in this environment/, ); }); @@ -2544,7 +2546,7 @@ describe('googleauth', () => { headers, new Headers({ authorization: `Bearer ${stsSuccessfulResponse.access_token}`, - }) + }), ); scopes.forEach(s => s.done()); }); @@ -2559,7 +2561,7 @@ describe('googleauth', () => { opts.headers, new Headers({ authorization: `Bearer ${stsSuccessfulResponse.access_token}`, - }) + }), ); scopes.forEach(s => s.done()); }); @@ -2576,7 +2578,7 @@ describe('googleauth', () => { authorization: `Bearer ${stsSuccessfulResponse.access_token}`, }, }) - .reply(200, data) + .reply(200, data), ); const auth = new GoogleAuth({keyFilename}); @@ -2621,7 +2623,7 @@ describe('googleauth', () => { describe('fromStream()', () => { it('should read the stream and create a client', async () => { const stream = fs.createReadStream( - './test/fixtures/external-account-authorized-user-cred.json' + './test/fixtures/external-account-authorized-user-cred.json', ); const actualClient = await auth.fromStream(stream); @@ -2633,7 +2635,7 @@ describe('googleauth', () => { it('should use environment variable when it is set', async () => { mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-authorized-user-cred.json' + './test/fixtures/external-account-authorized-user-cred.json', ); const res = await auth.getApplicationDefault(); @@ -2644,7 +2646,7 @@ describe('googleauth', () => { it('should use well-known file when it is available and env const is not set', async () => { mockLinuxWellKnownFile( - './test/fixtures/external-account-authorized-user-cred.json' + './test/fixtures/external-account-authorized-user-cred.json', ); const res = await auth.getApplicationDefault(); @@ -2658,7 +2660,7 @@ describe('googleauth', () => { it('should correctly read the file and create a valid client', async () => { const actualClient = await auth._getApplicationCredentialsFromFilePath( - './test/fixtures/external-account-authorized-user-cred.json' + './test/fixtures/external-account-authorized-user-cred.json', ); assert(actualClient instanceof ExternalAccountAuthorizedUserClient); @@ -2688,7 +2690,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-authorized-user-cred.json' + './test/fixtures/external-account-authorized-user-cred.json', ); const auth = new GoogleAuth(); const actualClient = await auth.getClient(); @@ -2700,7 +2702,7 @@ describe('googleauth', () => { // Set up a mock to return path to a valid credentials file. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/external-account-authorized-user-cred.json' + './test/fixtures/external-account-authorized-user-cred.json', ); const auth = new GoogleAuth(); @@ -2728,7 +2730,7 @@ describe('googleauth', () => { await assert.rejects( auth.sign('abc123'), - /Cannot sign data without `client_email`/ + /Cannot sign data without `client_email`/, ); }); }); @@ -2751,7 +2753,7 @@ describe('googleauth', () => { const headers = await jwt.getRequestHeaders(); assert.deepStrictEqual( headers.get('authorization'), - 'Bearer initial-access-token' + 'Bearer initial-access-token', ); scope.done(); assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); @@ -2773,7 +2775,7 @@ describe('googleauth', () => { const headers = await jwt.getRequestHeaders(); assert.deepStrictEqual( headers.get('authorization'), - 'Bearer initial-access-token' + 'Bearer initial-access-token', ); scope.done(); assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); @@ -2795,7 +2797,7 @@ describe('googleauth', () => { const headers = await jwt.getRequestHeaders(); assert.deepStrictEqual( headers.get('authorization'), - 'Bearer initial-access-token' + 'Bearer initial-access-token', ); scope.done(); assert.strictEqual('http://foo', (jwt as JWT).gtoken!.scope); diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 2f05d1b1..4b5f2d44 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -23,10 +23,7 @@ import { SubjectTokenSupplier, } from '../src/auth/identitypoolclient'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; -import { - BaseExternalAccountClient, - ExternalAccountSupplierContext, -} from '../src/auth/baseexternalclient'; +import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; import { assertGaxiosResponsePresent, getAudience, @@ -50,7 +47,7 @@ function escapeRegExp(str: string): string { describe('IdentityPoolClient', () => { const fileSubjectToken = fs.readFileSync( './test/fixtures/external-subject-token.txt', - 'utf-8' + 'utf-8', ); const audience = getAudience(); const crypto = createCrypto(); @@ -67,7 +64,7 @@ describe('IdentityPoolClient', () => { { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - fileSourcedOptions + fileSourcedOptions, ); const fileSourcedOptionsWithWorkforceUserProject = Object.assign( {}, @@ -77,20 +74,20 @@ describe('IdentityPoolClient', () => { audience: '//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc', subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - } + }, ); const fileSourcedOptionsWithClientAuthAndWorkforceUserProject = Object.assign( { client_id: 'CLIENT_ID', client_secret: 'SECRET', }, - fileSourcedOptionsWithWorkforceUserProject + fileSourcedOptionsWithWorkforceUserProject, ); const fileSourcedOptionsWithWorkforceUserProjectAndSA = Object.assign( { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - fileSourcedOptionsWithWorkforceUserProject + fileSourcedOptionsWithWorkforceUserProject, ); const basicAuthCreds = `${fileSourcedOptionsWithClientAuthAndWorkforceUserProject.client_id}:` + @@ -112,7 +109,7 @@ describe('IdentityPoolClient', () => { { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - jsonFileSourcedOptions + jsonFileSourcedOptions, ); const fileSourcedOptionsNotFound = { type: 'external_account', @@ -144,7 +141,7 @@ describe('IdentityPoolClient', () => { { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - urlSourcedOptions + urlSourcedOptions, ); const jsonRespUrlSourcedOptions: IdentityPoolClientOptions = { type: 'external_account', @@ -164,7 +161,7 @@ describe('IdentityPoolClient', () => { { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - jsonRespUrlSourcedOptions + jsonRespUrlSourcedOptions, ); const stsSuccessfulResponse: StsSuccessfulResponse = { access_token: 'ACCESS_TOKEN', @@ -199,16 +196,16 @@ describe('IdentityPoolClient', () => { ]; const invalidWorkforceIdentityPoolFileSourceOptions = Object.assign( {}, - fileSourcedOptionsWithWorkforceUserProject + fileSourcedOptionsWithWorkforceUserProject, ); const expectedWorkforcePoolUserProjectError = new Error( 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' + 'credentials.', ); it('should throw when neither file or url sources are provided', () => { const expectedError = new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.' + 'No valid Identity Pool "credential_source" provided, must be either file or url.', ); const invalidOptions = { type: 'external_account', @@ -228,7 +225,7 @@ describe('IdentityPoolClient', () => { it('should throw when both file and url options are provided', () => { const expectedError = new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.' + 'No valid Identity Pool "credential_source" provided, must be either file or url.', ); const invalidOptions = { type: 'external_account', @@ -270,7 +267,7 @@ describe('IdentityPoolClient', () => { it('should throw on required credential_source.format.subject_token_field_name', () => { const expectedError = new Error( - 'Missing subject_token_field_name for JSON credential_source format' + 'Missing subject_token_field_name for JSON credential_source format', ); const invalidOptions: IdentityPoolClientOptions = { type: 'external_account', @@ -299,16 +296,16 @@ describe('IdentityPoolClient', () => { assert.throws(() => { return new IdentityPoolClient( - invalidWorkforceIdentityPoolFileSourceOptions + invalidWorkforceIdentityPoolFileSourceOptions, ); }, expectedWorkforcePoolUserProjectError); }); - } + }, ); it('should throw when neither a credential source or a supplier is provided', () => { const expectedError = new Error( - 'A credential source or subject token supplier must be specified.' + 'A credential source or subject token supplier must be specified.', ); const invalidOptions = { type: 'external_account', @@ -325,7 +322,7 @@ describe('IdentityPoolClient', () => { it('should throw when both a credential source and a supplier is provided', () => { const expectedError = new Error( - 'Only one of credential source or subject token supplier can be specified.' + 'Only one of credential source or subject token supplier can be specified.', ); const invalidOptions = { type: 'external_account', @@ -383,7 +380,7 @@ describe('IdentityPoolClient', () => { ]; const validWorkforceIdentityPoolFileSourceOptions = Object.assign( {}, - fileSourcedOptionsWithWorkforceUserProject + fileSourcedOptionsWithWorkforceUserProject, ); for (const validWorkforceIdentityPoolClientAudience of validWorkforceIdentityPoolClientAudiences) { validWorkforceIdentityPoolFileSourceOptions.audience = @@ -391,7 +388,7 @@ describe('IdentityPoolClient', () => { assert.doesNotThrow(() => { return new IdentityPoolClient( - validWorkforceIdentityPoolFileSourceOptions + validWorkforceIdentityPoolFileSourceOptions, ); }); } @@ -416,7 +413,7 @@ describe('IdentityPoolClient', () => { it('should reject when the json subject_token_field_name is not found', async () => { const expectedError = new Error( - 'Unable to parse the subject_token from the credential_source file' + 'Unable to parse the subject_token from the credential_source file', ); const invalidOptions: IdentityPoolClientOptions = { type: 'external_account', @@ -444,8 +441,8 @@ describe('IdentityPoolClient', () => { client.retrieveSubjectToken(), new RegExp( `The file at ${escapeRegExp(invalidFile)} does not exist, ` + - 'or it is not a file' - ) + 'or it is not a file', + ), ); }); @@ -456,7 +453,7 @@ describe('IdentityPoolClient', () => { file: './test/fixtures', }; const invalidFile = fs.realpathSync( - invalidOptions.credential_source.file + invalidOptions.credential_source.file, ); const client = new IdentityPoolClient(invalidOptions); @@ -464,8 +461,8 @@ describe('IdentityPoolClient', () => { client.retrieveSubjectToken(), new RegExp( `The file at ${escapeRegExp(invalidFile)} does not exist, ` + - 'or it is not a file' - ) + 'or it is not a file', + ), ); }); }); @@ -522,13 +519,13 @@ describe('IdentityPoolClient', () => { ], { authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds + basicAuthCreds, )}`, - } + }, ); const client = new IdentityPoolClient( - fileSourcedOptionsWithClientAuthAndWorkforceUserProject + fileSourcedOptionsWithClientAuthAndWorkforceUserProject, ); const actualResponse = await client.getAccessToken(); @@ -565,7 +562,7 @@ describe('IdentityPoolClient', () => { ]); const client = new IdentityPoolClient( - fileSourcedOptionsWithWorkforceUserProject + fileSourcedOptionsWithWorkforceUserProject, ); const actualResponse = await client.getAccessToken(); @@ -598,14 +595,14 @@ describe('IdentityPoolClient', () => { ], { authorization: `Basic ${crypto.encodeBase64StringUtf8( - basicAuthCreds + basicAuthCreds, )}`, - } + }, ); const fileSourcedOptionsWithClientAuth: IdentityPoolClientOptions = Object.assign( {}, - fileSourcedOptionsWithClientAuthAndWorkforceUserProject + fileSourcedOptionsWithClientAuthAndWorkforceUserProject, ); delete fileSourcedOptionsWithClientAuth.workforce_pool_user_project; @@ -648,7 +645,7 @@ describe('IdentityPoolClient', () => { }), }, }, - ]) + ]), ); scopes.push( mockGenerateAccessToken({ @@ -656,11 +653,11 @@ describe('IdentityPoolClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new IdentityPoolClient( - fileSourcedOptionsWithWorkforceUserProjectAndSA + fileSourcedOptionsWithWorkforceUserProjectAndSA, ); const actualResponse = await client.getAccessToken(); @@ -702,7 +699,7 @@ describe('IdentityPoolClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new IdentityPoolClient(fileSourcedOptionsWithSA); @@ -776,7 +773,7 @@ describe('IdentityPoolClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new IdentityPoolClient(jsonFileSourcedOptionsWithSA); @@ -798,8 +795,8 @@ describe('IdentityPoolClient', () => { await assert.rejects( client.getAccessToken(), new RegExp( - `The file at ${invalidFile} does not exist, or it is not a file` - ) + `The file at ${invalidFile} does not exist, or it is not a file`, + ), ); }); @@ -825,9 +822,9 @@ describe('IdentityPoolClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'file', false, - false + false, ), - } + }, ); const client = new IdentityPoolClient(fileSourcedOptions); @@ -881,7 +878,7 @@ describe('IdentityPoolClient', () => { it('should reject when the json subject_token_field_name is not found', async () => { const expectedError = new Error( - 'Unable to parse the subject_token from the credential_source URL' + 'Unable to parse the subject_token from the credential_source URL', ); const invalidOptions: IdentityPoolClientOptions = { type: 'external_account', @@ -966,14 +963,14 @@ describe('IdentityPoolClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( nock(metadataBaseUrl, { reqheaders: metadataHeaders, }) .get(metadataPath) - .reply(200, externalSubjectToken) + .reply(200, externalSubjectToken), ); const client = new IdentityPoolClient(urlSourcedOptions); @@ -1023,7 +1020,7 @@ describe('IdentityPoolClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new IdentityPoolClient(urlSourcedOptionsWithSA); @@ -1060,14 +1057,14 @@ describe('IdentityPoolClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); scopes.push( nock(metadataBaseUrl, { reqheaders: metadataHeaders, }) .get(metadataPath) - .reply(200, jsonResponse) + .reply(200, jsonResponse), ); const client = new IdentityPoolClient(jsonRespUrlSourcedOptions); @@ -1120,7 +1117,7 @@ describe('IdentityPoolClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const client = new IdentityPoolClient(jsonRespUrlSourcedOptionsWithSA); @@ -1175,17 +1172,17 @@ describe('IdentityPoolClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'url', false, - false + false, ), - } - ) + }, + ), ); scopes.push( nock(metadataBaseUrl, { reqheaders: metadataHeaders, }) .get(metadataPath) - .reply(200, externalSubjectToken) + .reply(200, externalSubjectToken), ); const client = new IdentityPoolClient(urlSourcedOptions); @@ -1253,7 +1250,7 @@ describe('IdentityPoolClient', () => { subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', }, }, - ]) + ]), ); const options = { @@ -1305,7 +1302,7 @@ describe('IdentityPoolClient', () => { response: saSuccessResponse, token: stsSuccessfulResponse.access_token, scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }) + }), ); const options = { @@ -1368,10 +1365,10 @@ describe('IdentityPoolClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'programmatic', false, - false + false, ), - } - ) + }, + ), ); const options = { @@ -1410,7 +1407,7 @@ class TestSubjectTokenSupplier implements SubjectTokenSupplier { this.error = options.error; } - getSubjectToken(context: ExternalAccountSupplierContext): Promise { + getSubjectToken(): Promise { if (this.error) { throw this.error; } diff --git a/test/test.idtokenclient.ts b/test/test.idtokenclient.ts index b11332e3..0d17371c 100644 --- a/test/test.idtokenclient.ts +++ b/test/test.idtokenclient.ts @@ -70,7 +70,7 @@ describe('idtokenclient', () => { assert.strictEqual(client.credentials.id_token, 'abc123'); assert.deepStrictEqual( headers, - new Headers({authorization: 'Bearer abc123'}) + new Headers({authorization: 'Bearer abc123'}), ); }); @@ -92,7 +92,7 @@ describe('idtokenclient', () => { assert.strictEqual(client.credentials.id_token, 'abc123'); assert.deepStrictEqual( headers, - new Headers({authorization: 'Bearer abc123'}) + new Headers({authorization: 'Bearer abc123'}), ); }); }); diff --git a/test/test.impersonated.ts b/test/test.impersonated.ts index 375bb22c..71d5bd84 100644 --- a/test/test.impersonated.ts +++ b/test/test.impersonated.ts @@ -70,7 +70,7 @@ describe('impersonated', () => { 'https://www.googleapis.com/auth/cloud-platform', ]); return true; - } + }, ) .reply(200, { accessToken: 'qwerty345', @@ -89,7 +89,7 @@ describe('impersonated', () => { assert.strictEqual(impersonated.credentials.access_token, 'qwerty345'); assert.strictEqual( impersonated.credentials.expiry_date, - tomorrow.getTime() + tomorrow.getTime(), ); scopes.forEach(s => s.done()); }); @@ -115,7 +115,7 @@ describe('impersonated', () => { 'https://www.googleapis.com/auth/cloud-platform', ]); return true; - } + }, ) .reply(200, { accessToken: 'universe-token', @@ -183,7 +183,7 @@ describe('impersonated', () => { 'https://www.googleapis.com/auth/cloud-platform', ]); return true; - } + }, ) .reply(200, { accessToken: 'qwerty345', @@ -203,7 +203,7 @@ describe('impersonated', () => { assert.strictEqual(impersonated.credentials.access_token, 'qwerty345'); assert.strictEqual( impersonated.credentials.expiry_date, - tomorrow.getTime() + tomorrow.getTime(), ); scopes.forEach(s => s.done()); }); @@ -225,7 +225,7 @@ describe('impersonated', () => { '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', () => { return true; - } + }, ) .reply(200, { accessToken: 'qwerty345', @@ -236,7 +236,7 @@ describe('impersonated', () => { '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', () => { return true; - } + }, ) .reply(200, { accessToken: 'qwerty456', @@ -260,7 +260,7 @@ describe('impersonated', () => { assert.strictEqual(impersonated.credentials.access_token, 'qwerty456'); assert.strictEqual( impersonated.credentials.expiry_date, - tomorrow.getTime() + tomorrow.getTime(), ); scopes.forEach(s => s.done()); }); @@ -283,7 +283,7 @@ describe('impersonated', () => { 'https://www.googleapis.com/auth/cloud-platform', ]); return true; - } + }, ) .reply(200, { accessToken: 'qwerty345', @@ -307,7 +307,7 @@ describe('impersonated', () => { assert.strictEqual(impersonated.credentials.access_token, 'qwerty345'); assert.strictEqual( impersonated.credentials.expiry_date, - tomorrow.getTime() + tomorrow.getTime(), ); scopes.forEach(s => s.done()); }); @@ -321,7 +321,7 @@ describe('impersonated', () => { }), nock('https://iamcredentials.googleapis.com') .post( - '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken' + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', ) .reply(404, { error: { @@ -354,7 +354,7 @@ describe('impersonated', () => { }), nock('https://iamcredentials.googleapis.com') .post( - '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken' + '/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken', ) .reply(500), ]; @@ -407,7 +407,7 @@ describe('impersonated', () => { 'https://www.googleapis.com/auth/cloud-platform', ]); return true; - } + }, ) .reply(200, { accessToken: 'qwerty345', @@ -428,7 +428,7 @@ describe('impersonated', () => { assert.strictEqual(headers.get('authorization'), 'Bearer qwerty345'); assert.strictEqual( impersonated.credentials.expiry_date, - tomorrow.getTime() + tomorrow.getTime(), ); scopes.forEach(s => s.done()); }); @@ -457,7 +457,7 @@ describe('impersonated', () => { assert.deepStrictEqual(body.delegates, expectedDeligates); assert.strictEqual(body.useEmailAzp, true); return true; - } + }, ) .reply(200, { token: expectedToken, @@ -501,7 +501,7 @@ describe('impersonated', () => { assert.strictEqual(body.includeEmail, expectedIncludeEmail); assert.deepStrictEqual(body.delegates, expectedDeligates); return true; - } + }, ) .reply(200, { token: expectedToken, @@ -542,11 +542,11 @@ describe('impersonated', () => { (body: {delegates: string[]; payload: string}) => { assert.strictEqual( body.payload, - Buffer.from(expectedBlobToSign).toString('base64') + Buffer.from(expectedBlobToSign).toString('base64'), ); assert.deepStrictEqual(body.delegates, expectedDeligates); return true; - } + }, ) .reply(200, { keyId: expectedKeyID, diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 0d12729e..df0db7ca 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -99,7 +99,7 @@ describe('jwt', () => { assert.strictEqual(PEM_PATH, jwt.gtoken!.keyFile); assert.strictEqual( ['http://bar', 'http://foo'].join(' '), - jwt.gtoken!.scope + jwt.gtoken!.scope, ); assert.strictEqual('bar@subjectaccount.com', jwt.gtoken!.sub); assert.strictEqual('initial-access-token', jwt.credentials.access_token); @@ -145,7 +145,7 @@ describe('jwt', () => { assert.strictEqual( 'initial-access-token', got, - 'the access token was wrong: ' + got + 'the access token was wrong: ' + got, ); done(); }); @@ -166,7 +166,8 @@ describe('jwt', () => { scope.done(); done(); }) - .getAccessToken(); + .getAccessToken() + .catch(console.error); }); it('can obtain new access token when scopes are set', async () => { @@ -186,7 +187,7 @@ describe('jwt', () => { assert.strictEqual( want, headers.get('authorization'), - `the authorization header was wrong: ${headers.get('authorization')}` + `the authorization header was wrong: ${headers.get('authorization')}`, ); }); @@ -491,7 +492,7 @@ describe('jwt', () => { const dateInMillis = new Date().getTime(); assert.strictEqual( dateInMillis.toString().length, - jwt.credentials.expiry_date!.toString().length + jwt.credentials.expiry_date!.toString().length, ); }); @@ -678,7 +679,7 @@ describe('jwt', () => { // Read the contents of the file into a json object. const fileContents = fs.readFileSync( './test/fixtures/private.json', - 'utf-8' + 'utf-8', ); const json = JSON.parse(fileContents); @@ -743,7 +744,7 @@ describe('jwt', () => { require('../../test/fixtures/service-account-with-quota.json'), { private_key: keypair(512 /* bitsize of private key */).private, - } + }, ), }); const client = await auth.getClient(); @@ -751,11 +752,11 @@ describe('jwt', () => { // If a URL isn't provided to authorize, the OAuth2Client super class is // executed, which was already exercised. const headers = await client.getRequestHeaders( - 'http:/example.com/my_test_service' + 'http:/example.com/my_test_service', ); assert.strictEqual( headers.get('x-goog-user-project'), - 'fake-quota-project' + 'fake-quota-project', ); }); @@ -835,7 +836,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - undefined + undefined, ); }); @@ -857,7 +858,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - undefined + undefined, ); }); @@ -879,7 +880,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - undefined + undefined, ); }); @@ -902,7 +903,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - undefined + undefined, ); }); @@ -924,7 +925,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - ['scope1', 'scope2'] + ['scope1', 'scope2'], ); }); @@ -968,7 +969,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - ['scope1', 'scope2'] + ['scope1', 'scope2'], ); }); @@ -990,7 +991,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - ['scope1', 'scope2'] + ['scope1', 'scope2'], ); }); @@ -1013,7 +1014,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - ['scope1', 'scope2'] + ['scope1', 'scope2'], ); }); @@ -1035,7 +1036,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - ['scope1', 'scope2'] + ['scope1', 'scope2'], ); }); @@ -1058,7 +1059,7 @@ describe('jwt', () => { stubGetRequestHeaders, 'https//beepboop.googleapis.com', undefined, - ['scope1', 'scope2'] + ['scope1', 'scope2'], ); }); @@ -1079,7 +1080,7 @@ describe('jwt', () => { await assert.rejects( () => jwt.getRequestHeaders('https//beepboop.googleapis.com'), - /Domain-wide delegation is not supported in universes other than/ + /Domain-wide delegation is not supported in universes other than/, ); }); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index fc542017..22d755cd 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -179,7 +179,7 @@ describe('jwtaccess', () => { // Read the contents of the file into a json object. const fileContents = fs.readFileSync( './test/fixtures/private.json', - 'utf-8' + 'utf-8', ); const json = JSON.parse(fileContents); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 9dc0a575..b8d2818a 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -45,19 +45,19 @@ describe('oauth2', () => { const certsPath = '/oauth2/v1/certs'; const certsResPath = path.join( __dirname, - '../../test/fixtures/oauthcertspem.json' + '../../test/fixtures/oauthcertspem.json', ); const publicKeyEcdsa = fs.readFileSync( './test/fixtures/fake-ecdsa-public.pem', - 'utf-8' + 'utf-8', ); const privateKeyEcdsa = fs.readFileSync( './test/fixtures/fake-ecdsa-private.pem', - 'utf-8' + 'utf-8', ); const pubkeysResPath = path.join( __dirname, - '../../test/fixtures/ecdsapublickeys.json' + '../../test/fixtures/ecdsapublickeys.json', ); describe(__filename, () => { @@ -115,7 +115,7 @@ describe('oauth2', () => { }; assert.throws( () => client.generateAuthUrl(opts), - /If a code_challenge_method is provided, code_challenge must be included/ + /If a code_challenge_method is provided, code_challenge must be included/, ); }); @@ -139,7 +139,7 @@ describe('oauth2', () => { assert.strictEqual(props.get('code_challenge'), codes.codeChallenge); assert.strictEqual( props.get('code_challenge_method'), - CodeChallengeMethod.S256 + CodeChallengeMethod.S256, ); }); @@ -163,7 +163,7 @@ describe('oauth2', () => { certs: {}, requiredAudience: string | string[], issuers?: string[], - theMaxExpiry?: number + theMaxExpiry?: number, ) => { assert.strictEqual(jwt, idToken); assert.deepStrictEqual(certs, fakeCerts); @@ -194,7 +194,7 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync = async ( jwt: string, certs: {}, - requiredAudience: string + requiredAudience: string, ) => { assert.strictEqual(jwt, idToken); assert.deepStrictEqual(certs, fakeCerts); @@ -204,7 +204,7 @@ describe('oauth2', () => { assert.throws( // eslint-disable-next-line @typescript-eslint/no-explicit-any () => (client as any).verifyIdToken(idToken, audience), - /This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry./ + /This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry./, ); }); @@ -256,7 +256,7 @@ describe('oauth2', () => { const login = await client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ); assert.strictEqual(login.getUserId(), '123456789'); }); @@ -294,9 +294,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /Wrong recipient/ + /Wrong recipient/, ); }); @@ -334,9 +334,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - validAudiences + validAudiences, ), - /Wrong recipient/ + /Wrong recipient/, ); }); @@ -367,9 +367,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /Wrong number of segments/ + /Wrong number of segments/, ); }); @@ -405,9 +405,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /Can't parse token envelope/ + /Can't parse token envelope/, ); }); @@ -443,9 +443,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /Can't parse token payload/ + /Can't parse token payload/, ); }); @@ -479,9 +479,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /Invalid token signature/ + /Invalid token signature/, ); }); @@ -512,9 +512,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /No expiration time/ + /No expiration time/, ); }); @@ -547,9 +547,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /No issue time/ + /No issue time/, ); }); @@ -585,9 +585,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /Expiration time too far in future/ + /Expiration time too far in future/, ); }); @@ -625,7 +625,7 @@ describe('oauth2', () => { {keyid: publicKey}, 'testaudience', ['testissuer'], - maxExpiry + maxExpiry, ); }); @@ -663,9 +663,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /Token used too early/ + /Token used too early/, ); }); @@ -704,9 +704,9 @@ describe('oauth2', () => { client.verifySignedJwtWithCertsAsync( data, {keyid: publicKey}, - 'testaudience' + 'testaudience', ), - /Token used too late/ + /Token used too late/, ); }); @@ -743,9 +743,9 @@ describe('oauth2', () => { data, {keyid: publicKey}, 'testaudience', - ['testissuer'] + ['testissuer'], ), - /Invalid issuer/ + /Invalid issuer/, ); }); @@ -781,7 +781,7 @@ describe('oauth2', () => { data, {keyid: publicKey}, 'testaudience', - ['testissuer'] + ['testissuer'], ); }); @@ -813,14 +813,14 @@ describe('oauth2', () => { signer.update(data); const signature = formatEcdsa.derToJose( signer.sign(privateKeyEcdsa, 'base64'), - 'ES256' + 'ES256', ); data += '.' + signature; await client.verifySignedJwtWithCertsAsync( data, {keyid: publicKeyEcdsa}, 'testaudience', - ['testissuer'] + ['testissuer'], ); }); @@ -832,11 +832,11 @@ describe('oauth2', () => { assert.strictEqual(err, null); assert.notStrictEqual( certs!['a15eea964ab9cce480e5ef4f47cb17b9fa7d0b21'], - null + null, ); assert.notStrictEqual( certs!['39596dc3a3f12aa74b481579e4ec944f86d24b95'], - null + null, ); scope.done(); done(); @@ -902,7 +902,7 @@ describe('oauth2', () => { client.request({}, (err, result) => { assert.strictEqual( err!.message, - 'No access, refresh token, API key or refresh handler callback is set.' + 'No access, refresh token, API key or refresh handler callback is set.', ); assert.strictEqual(result, undefined); done(); @@ -1103,7 +1103,7 @@ describe('oauth2', () => { await client.request({url: 'http://example.com'}); assert.strictEqual( 'initial-access-token', - client.credentials.access_token + client.credentials.access_token, ); assert.strictEqual(false as boolean, scopes[0].isDone()); scopes[1].done(); @@ -1119,7 +1119,7 @@ describe('oauth2', () => { client.request({url: 'http://example.com'}, () => { assert.strictEqual( 'initial-access-token', - client.credentials.access_token + client.credentials.access_token, ); assert.strictEqual(false as boolean, scopes[0].isDone()); scopes[1].done(); @@ -1136,7 +1136,7 @@ describe('oauth2', () => { client.request({url: 'http://example.com'}, () => { assert.strictEqual( 'initial-access-token', - client.credentials.access_token + client.credentials.access_token, ); assert.strictEqual(false as boolean, scopes[0].isDone()); scopes[1].done(); @@ -1251,11 +1251,11 @@ describe('oauth2', () => { scopes.forEach(s => s.done()); assert.strictEqual( client.credentials.access_token, - expectedRefreshedAccessToken.access_token + expectedRefreshedAccessToken.access_token, ); assert.strictEqual( client.credentials.expiry_date, - expectedRefreshedAccessToken.expiry_date + expectedRefreshedAccessToken.expiry_date, ); }); }); @@ -1289,11 +1289,11 @@ describe('oauth2', () => { scopes.forEach(s => s.done()); assert.strictEqual( client.credentials.access_token, - expectedRefreshedAccessToken.access_token + expectedRefreshedAccessToken.access_token, ); assert.strictEqual( client.credentials.expiry_date, - expectedRefreshedAccessToken.expiry_date + expectedRefreshedAccessToken.expiry_date, ); }); }); @@ -1315,7 +1315,7 @@ describe('oauth2', () => { assert(e); assert.strictEqual(e.response!.status, 401); done(); - } + }, ); }); @@ -1506,7 +1506,7 @@ describe('oauth2', () => { const oauth2client = new OAuth2Client(opts); assert.equal( oauth2client.clientAuthentication, - ClientAuthentication.None + ClientAuthentication.None, ); try { @@ -1535,7 +1535,7 @@ describe('oauth2', () => { const oauth2client = new OAuth2Client(opts); assert.equal( oauth2client.clientAuthentication, - ClientAuthentication.ClientSecretPost + ClientAuthentication.ClientSecretPost, ); }); @@ -1649,7 +1649,7 @@ describe('oauth2', () => { await assert.rejects( client.getRequestHeaders('http://example.com'), - /No refresh token is set./ + /No refresh token is set./, ); }); @@ -1667,7 +1667,7 @@ describe('oauth2', () => { assert.strictEqual( refreshedAccessToken.token, - expectedRefreshedAccessToken.access_token + expectedRefreshedAccessToken.access_token, ); }); @@ -1688,7 +1688,7 @@ describe('oauth2', () => { assert.strictEqual( refreshedAccessToken.token, - expectedRefreshedAccessToken.access_token + expectedRefreshedAccessToken.access_token, ); }); @@ -1707,7 +1707,7 @@ describe('oauth2', () => { await assert.rejects( client.getAccessToken(), - /No access token is returned by the refreshHandler callback./ + /No access token is returned by the refreshHandler callback./, ); }); diff --git a/test/test.oauth2common.ts b/test/test.oauth2common.ts index 17f7aaf2..1ff92abe 100644 --- a/test/test.oauth2common.ts +++ b/test/test.oauth2common.ts @@ -27,7 +27,7 @@ import { class TestOAuthClientAuthHandler extends OAuthClientAuthHandler { testApplyClientAuthenticationOptions( opts: GaxiosOptions, - bearerToken?: string + bearerToken?: string, ) { return this.applyClientAuthenticationOptions(opts, bearerToken); } @@ -112,7 +112,7 @@ describe('OAuthClientAuthHandler', () => { const expectedOptions = prepareExpectedOptions(options); expectedOptions.headers.set( 'authorization', - `Basic ${expectedBase64EncodedCred}` + `Basic ${expectedBase64EncodedCred}`, ); handler.testApplyClientAuthenticationOptions(options); @@ -136,7 +136,7 @@ describe('OAuthClientAuthHandler', () => { const expectedOptions = prepareExpectedOptions(options); expectedOptions.headers.set( 'authorization', - `Basic ${expectedBase64EncodedCredNoSecret}` + `Basic ${expectedBase64EncodedCredNoSecret}`, ); handler.testApplyClientAuthenticationOptions(options); @@ -156,7 +156,7 @@ describe('OAuthClientAuthHandler', () => { const expectedOptions = prepareExpectedOptions(options); expectedOptions.headers.set( 'authorization', - `Basic ${expectedBase64EncodedCred}` + `Basic ${expectedBase64EncodedCred}`, ); handler.testApplyClientAuthenticationOptions(options); @@ -177,7 +177,7 @@ describe('OAuthClientAuthHandler', () => { it(`should throw on requests with unsupported HTTP method ${method}`, () => { const expectedError = new Error( `${method || 'GET'} HTTP method does not support request-body ` + - 'client authentication' + 'client authentication', ); const handler = new TestOAuthClientAuthHandler(reqBodyAuth); const originalOptions: GaxiosOptions = { @@ -194,7 +194,7 @@ describe('OAuthClientAuthHandler', () => { it('should throw on unsupported content-types', () => { const expectedError = new Error( 'text/html content-types are not supported with request-body ' + - 'client authentication' + 'client authentication', ); const handler = new TestOAuthClientAuthHandler(reqBodyAuth); const originalOptions: GaxiosOptions = { @@ -447,7 +447,7 @@ describe('getErrorFromOAuthErrorResponse', () => { assert.strictEqual( error.message, `Error code ${resp.error}: ${resp.error_description} ` + - `- ${resp.error_uri}` + `- ${resp.error_uri}`, ); }); @@ -459,7 +459,7 @@ describe('getErrorFromOAuthErrorResponse', () => { const error = getErrorFromOAuthErrorResponse(resp); assert.strictEqual( error.message, - `Error code ${resp.error}: ${resp.error_description}` + `Error code ${resp.error}: ${resp.error_description}`, ); }); @@ -475,7 +475,7 @@ describe('getErrorFromOAuthErrorResponse', () => { const originalError = new CustomError( 'Original error message', 'Error stack', - '123456' + '123456', ); const resp = { error: 'unsupported_grant_type', @@ -486,7 +486,7 @@ describe('getErrorFromOAuthErrorResponse', () => { `Error code ${resp.error}: ${resp.error_description} ` + `- ${resp.error_uri}`, 'Error stack', - '123456' + '123456', ); const actualError = getErrorFromOAuthErrorResponse(resp, originalError); diff --git a/test/test.pluggableauthclient.ts b/test/test.pluggableauthclient.ts index 45cbaf8d..9b4abfc1 100644 --- a/test/test.pluggableauthclient.ts +++ b/test/test.pluggableauthclient.ts @@ -16,6 +16,7 @@ import * as assert from 'assert'; import { ExecutableError, PluggableAuthClient, + PluggableAuthClientOptions, } from '../src/auth/pluggable-auth-client'; import {BaseExternalAccountClient} from '../src'; import { @@ -67,7 +68,7 @@ describe('PluggableAuthClient', () => { { service_account_impersonation_url: getServiceAccountImpersonationUrl(), }, - pluggableAuthOptions + pluggableAuthOptions, ); const pluggableAuthCredentialSourceNoOutput = { executable: { @@ -124,12 +125,12 @@ describe('PluggableAuthClient', () => { fileStub = sandbox.stub( PluggableAuthHandler.prototype, - 'retrieveCachedResponse' + 'retrieveCachedResponse', ); executableStub = sandbox.stub( PluggableAuthHandler.prototype, - 'retrieveResponseFromExecutable' + 'retrieveResponseFromExecutable', ); }); @@ -147,7 +148,7 @@ describe('PluggableAuthClient', () => { describe('Constructor', () => { it('should throw when credential_source is missing executable', () => { const expectedError = new Error( - 'No valid Pluggable Auth "credential_source" provided.' + 'No valid Pluggable Auth "credential_source" provided.', ); const invalidCredentialSource = {}; const invalidOptions = { @@ -159,15 +160,15 @@ describe('PluggableAuthClient', () => { }; assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new PluggableAuthClient(invalidOptions); + return new PluggableAuthClient( + invalidOptions as unknown as PluggableAuthClientOptions, + ); }, expectedError); }); it('should throw when credential_source is missing command', () => { const expectedError = new Error( - 'No valid Pluggable Auth "credential_source" provided.' + 'No valid Pluggable Auth "credential_source" provided.', ); const invalidCredentialSource = { executable: { @@ -184,15 +185,15 @@ describe('PluggableAuthClient', () => { }; assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new PluggableAuthClient(invalidOptions); + return new PluggableAuthClient( + invalidOptions as unknown as PluggableAuthClientOptions, + ); }, expectedError); }); it('should throw when time_millis is below minimum allowed value', () => { const expectedError = new Error( - 'Timeout must be between 5000 and 120000 milliseconds.' + 'Timeout must be between 5000 and 120000 milliseconds.', ); const invalidCredentialSource = { executable: { @@ -216,7 +217,7 @@ describe('PluggableAuthClient', () => { it('should throw when time_millis is above maximum allowed value', () => { const expectedError = new Error( - 'Timeout must be between 5000 and 120000 milliseconds.' + 'Timeout must be between 5000 and 120000 milliseconds.', ); const invalidCredentialSource = { executable: { @@ -249,7 +250,7 @@ describe('PluggableAuthClient', () => { it('should throw when allow executables environment variables is not 1', async () => { process.env.GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES = '0'; const expectedError = new Error( - 'Pluggable Auth executables need to be explicitly allowed to run by setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment Variable to 1.' + 'Pluggable Auth executables need to be explicitly allowed to run by setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment Variable to 1.', ); const client = new PluggableAuthClient(pluggableAuthOptions); @@ -315,7 +316,7 @@ describe('PluggableAuthClient', () => { it('should throw error when version is not supported', async () => { responseJson.version = 99999; const expectedError = new Error( - 'Version of executable is not currently supported, maximum supported version is 1.' + 'Version of executable is not currently supported, maximum supported version is 1.', ); fileStub.resolves(undefined); executableStub.resolves(new ExecutableResponse(responseJson)); @@ -393,7 +394,7 @@ describe('PluggableAuthClient', () => { it('should throw error when output file response does not contain expiration_time and output file is specified', async () => { responseJson.expiration_time = undefined; const expectedError = new InvalidExpirationTimeFieldError( - 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.' + 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.', ); const client = new PluggableAuthClient(pluggableAuthOptions); fileStub.resolves(new ExecutableResponse(responseJson)); @@ -406,7 +407,7 @@ describe('PluggableAuthClient', () => { it('should throw error when executable response does not contain expiration_time and output file is specified', async () => { responseJson.expiration_time = undefined; const expectedError = new InvalidExpirationTimeFieldError( - 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.' + 'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.', ); const client = new PluggableAuthClient(pluggableAuthOptions); fileStub.resolves(undefined); @@ -422,7 +423,7 @@ describe('PluggableAuthClient', () => { expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', audience); expectedEnvMap.set( 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', - responseJson.token_type + responseJson.token_type, ); expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); const client = new PluggableAuthClient(pluggableAuthOptionsNoOutput); @@ -440,12 +441,12 @@ describe('PluggableAuthClient', () => { expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', audience); expectedEnvMap.set( 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', - responseJson.token_type + responseJson.token_type, ); expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); expectedEnvMap.set( 'GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE', - pluggableAuthCredentialSource.executable.output_file + pluggableAuthCredentialSource.executable.output_file, ); const client = new PluggableAuthClient(pluggableAuthOptions); fileStub.resolves(undefined); @@ -462,12 +463,12 @@ describe('PluggableAuthClient', () => { expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', audience); expectedEnvMap.set( 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', - responseJson.token_type + responseJson.token_type, ); expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); expectedEnvMap.set( 'GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE', - pluggableAuthCredentialSource.executable.output_file + pluggableAuthCredentialSource.executable.output_file, ); expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL', saEmail); const client = new PluggableAuthClient(pluggableAuthOptionsWithSA); @@ -512,9 +513,9 @@ describe('PluggableAuthClient', () => { 'x-goog-api-client': getExpectedExternalAccountMetricsHeaderValue( 'executable', false, - false + false, ), - } + }, ); const client = new PluggableAuthClient(pluggableAuthOptionsOIDC); diff --git a/test/test.pluggableauthhandler.ts b/test/test.pluggableauthhandler.ts index 7a375aa8..ad442887 100644 --- a/test/test.pluggableauthhandler.ts +++ b/test/test.pluggableauthhandler.ts @@ -56,9 +56,9 @@ describe('PluggableAuthHandler', () => { }; assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new PluggableAuthHandler(invalidOptions); + return new PluggableAuthHandler( + invalidOptions as unknown as PluggableAuthHandlerOptions, + ); }, expectedError); }); @@ -70,15 +70,15 @@ describe('PluggableAuthHandler', () => { }; assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new PluggableAuthHandler(invalidOptions); + return new PluggableAuthHandler( + invalidOptions as unknown as PluggableAuthHandlerOptions, + ); }, expectedError); }); it('should throw when command cannot be parsed', async () => { const expectedError = new Error( - 'Provided command: " " could not be parsed.' + 'Provided command: " " could not be parsed.', ); const invalidHandlerOptions = { command: ' ', @@ -108,7 +108,7 @@ describe('PluggableAuthHandler', () => { const expectedEnvMap = new Map(); expectedEnvMap.set( 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', - SAML_SUBJECT_TOKEN_TYPE + SAML_SUBJECT_TOKEN_TYPE, ); expectedEnvMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0'); const expectedOpts = { @@ -152,7 +152,7 @@ describe('PluggableAuthHandler', () => { await assert.rejects( handler.retrieveResponseFromExecutable(new Map()), - expectedError + expectedError, ); }); @@ -160,14 +160,14 @@ describe('PluggableAuthHandler', () => { const handler = new PluggableAuthHandler(defaultHandlerOptions); const response = handler.retrieveResponseFromExecutable( - new Map() + new Map(), ); spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); spawnEvent.emit('close', 0); assert.deepEqual( await response, - new ExecutableResponse(defaultResponseJson) + new ExecutableResponse(defaultResponseJson), ); }); @@ -177,14 +177,14 @@ describe('PluggableAuthHandler', () => { defaultResponseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; defaultResponseJson.id_token = 'subject token'; const response = handler.retrieveResponseFromExecutable( - new Map() + new Map(), ); spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); spawnEvent.emit('close', 0); assert.deepEqual( await response, - new ExecutableResponse(defaultResponseJson) + new ExecutableResponse(defaultResponseJson), ); }); @@ -192,14 +192,14 @@ describe('PluggableAuthHandler', () => { const handler = new PluggableAuthHandler(defaultHandlerOptions); defaultResponseJson.expiration_time = undefined; const response = handler.retrieveResponseFromExecutable( - new Map() + new Map(), ); spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); spawnEvent.emit('close', 0); assert.deepEqual( await response, - new ExecutableResponse(defaultResponseJson) + new ExecutableResponse(defaultResponseJson), ); }); @@ -210,14 +210,14 @@ describe('PluggableAuthHandler', () => { defaultResponseJson.token_type = OIDC_SUBJECT_TOKEN_TYPE1; defaultResponseJson.id_token = 'subject token'; const response = handler.retrieveResponseFromExecutable( - new Map() + new Map(), ); spawnEvent.stdout!.emit('data', JSON.stringify(defaultResponseJson)); spawnEvent.emit('close', 0); assert.deepEqual( await response, - new ExecutableResponse(defaultResponseJson) + new ExecutableResponse(defaultResponseJson), ); }); @@ -236,7 +236,7 @@ describe('PluggableAuthHandler', () => { spawnStub, expectedCommand, expectedArgs, - expectedOpts + expectedOpts, ); }); @@ -259,7 +259,7 @@ describe('PluggableAuthHandler', () => { spawnStub, expectedCommand, expectedArgs, - expectedOpts + expectedOpts, ); }); @@ -282,7 +282,7 @@ describe('PluggableAuthHandler', () => { spawnStub, expectedCommand, expectedArgs, - expectedOpts + expectedOpts, ); }); @@ -291,7 +291,7 @@ describe('PluggableAuthHandler', () => { const handler = new PluggableAuthHandler(defaultHandlerOptions); const response = handler.retrieveResponseFromExecutable( - new Map() + new Map(), ); spawnEvent.stderr!.emit('data', 'test error'); spawnEvent.emit('close', 1); @@ -301,7 +301,7 @@ describe('PluggableAuthHandler', () => { it('should throw error when executable times out', async () => { const expectedError = new Error( - 'The executable failed to finish within the timeout specified.' + 'The executable failed to finish within the timeout specified.', ); spawnEvent.kill = () => { return true; @@ -309,7 +309,7 @@ describe('PluggableAuthHandler', () => { const handler = new PluggableAuthHandler(defaultHandlerOptions); const response = handler.retrieveResponseFromExecutable( - new Map() + new Map(), ); clock.tick(10001); @@ -318,12 +318,12 @@ describe('PluggableAuthHandler', () => { it('should throw error when non-json text is returned', async () => { const expectedError = new ExecutableResponseError( - 'The executable returned an invalid response: THIS_IS_NOT_JSON' + 'The executable returned an invalid response: THIS_IS_NOT_JSON', ); const handler = new PluggableAuthHandler(defaultHandlerOptions); const response = handler.retrieveResponseFromExecutable( - new Map() + new Map(), ); spawnEvent.stdout!.emit('data', 'THIS_IS_NOT_JSON'); spawnEvent.emit('close', 0); @@ -333,7 +333,7 @@ describe('PluggableAuthHandler', () => { it('should throw ExecutableResponseError', async () => { const expectedError = new InvalidSuccessFieldError( - "Executable response must contain a 'success' field." + "Executable response must contain a 'success' field.", ); const handler = new PluggableAuthHandler(defaultHandlerOptions); const invalidResponse = { @@ -344,7 +344,7 @@ describe('PluggableAuthHandler', () => { }; const response = handler.retrieveResponseFromExecutable( - new Map() + new Map(), ); spawnEvent.stdout!.emit('data', JSON.stringify(invalidResponse)); spawnEvent.emit('close', 0); @@ -403,7 +403,7 @@ describe('PluggableAuthHandler', () => { assert.deepEqual( await response, - new ExecutableResponse(defaultResponseJson) + new ExecutableResponse(defaultResponseJson), ); }); @@ -419,7 +419,7 @@ describe('PluggableAuthHandler', () => { assert.deepEqual( await response, - new ExecutableResponse(defaultResponseJson) + new ExecutableResponse(defaultResponseJson), ); }); @@ -484,7 +484,7 @@ describe('PluggableAuthHandler', () => { it('should throw error when non-json text is returned', async () => { const expectedError = new ExecutableResponseError( - 'The output file contained an invalid response: THIS_IS_NOT_JSON' + 'The output file contained an invalid response: THIS_IS_NOT_JSON', ); const handler = new PluggableAuthHandler(defaultHandlerOptions); @@ -496,7 +496,7 @@ describe('PluggableAuthHandler', () => { it('should throw ExecutableResponseError', async () => { const expectedError = new InvalidSuccessFieldError( - "Executable response must contain a 'success' field." + "Executable response must contain a 'success' field.", ); const handler = new PluggableAuthHandler(defaultHandlerOptions); const invalidResponse = { diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 8522c3d4..79f3f4f2 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -115,7 +115,7 @@ describe('refresh', () => { // Read the contents of the file into a json object. const fileContents = fs.readFileSync( './test/fixtures/refresh.json', - 'utf-8' + 'utf-8', ); const json = JSON.parse(fileContents); @@ -143,7 +143,7 @@ describe('refresh', () => { // Fake loading default credentials with quota project set: const stream = fs.createReadStream( - './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json', ); const refresh = new UserRefreshClient(); await refresh.fromStream(stream); @@ -155,7 +155,7 @@ describe('refresh', () => { it('getRequestHeaders should populate x-goog-user-project header if quota_project_id present and token has not expired', async () => { const stream = fs.createReadStream( - './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json', ); const eagerRefreshThresholdMillis = 10; const refresh = new UserRefreshClient({ @@ -176,7 +176,7 @@ describe('refresh', () => { .post('/token') .reply(200, {}); const stream = fs.createReadStream( - './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json' + './test/fixtures/config-with-quota/.config/gcloud/application_default_credentials.json', ); const refresh = new UserRefreshClient(); await refresh.fromStream(stream); diff --git a/test/test.stscredentials.ts b/test/test.stscredentials.ts index b183e8af..c78ea056 100644 --- a/test/test.stscredentials.ts +++ b/test/test.stscredentials.ts @@ -91,13 +91,13 @@ describe('StsCredentials', () => { response: StsSuccessfulResponse | OAuthErrorResponse, // eslint-disable-next-line @typescript-eslint/no-explicit-any request: {[key: string]: any}, - additionalHeaders?: {[key: string]: string} + additionalHeaders?: {[key: string]: string}, ): nock.Scope { const headers = Object.assign( { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, - additionalHeaders || {} + additionalHeaders || {}, ); return nock(baseUrl) .post(path, request, { @@ -149,7 +149,7 @@ describe('StsCredentials', () => { { client_id: requestBodyAuth.clientId, client_secret: requestBodyAuth.clientSecret, - } + }, ); describe('without client authentication', () => { @@ -158,14 +158,14 @@ describe('StsCredentials', () => { 200, stsSuccessfulResponse, expectedRequest, - additionalHeaders + additionalHeaders, ); const stsCredentials = new StsCredentials(tokenExchangeEndpoint); const resp = await stsCredentials.exchangeToken( stsCredentialsOptions, additionalHeaders, - options + options, ); // Confirm raw GaxiosResponse appended to response. @@ -179,12 +179,12 @@ describe('StsCredentials', () => { const scope = mockStsTokenExchange( 200, stsSuccessfulResponse, - expectedPartialRequest + expectedPartialRequest, ); const stsCredentials = new StsCredentials(tokenExchangeEndpoint); const resp = await stsCredentials.exchangeToken( - partialStsCredentialsOptions + partialStsCredentialsOptions, ); // Confirm raw GaxiosResponse appended to response. @@ -199,7 +199,7 @@ describe('StsCredentials', () => { 400, errorResponse, expectedRequest, - additionalHeaders + additionalHeaders, ); const expectedError = getErrorFromOAuthErrorResponse(errorResponse); const stsCredentials = new StsCredentials(tokenExchangeEndpoint); @@ -208,9 +208,9 @@ describe('StsCredentials', () => { stsCredentials.exchangeToken( stsCredentialsOptions, additionalHeaders, - options + options, ), - expectedError + expectedError, ); scope.done(); }); @@ -225,7 +225,7 @@ describe('StsCredentials', () => { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, - } + }, ) .replyWithError('ETIMEDOUT') .post( @@ -236,7 +236,7 @@ describe('StsCredentials', () => { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', }, - } + }, ) .reply(200, stsSuccessfulResponse); const stsCredentials = new StsCredentials(tokenExchangeEndpoint); @@ -244,7 +244,7 @@ describe('StsCredentials', () => { const resp = await stsCredentials.exchangeToken( stsCredentialsOptions, additionalHeaders, - options + options, ); assertGaxiosResponsePresent(resp); @@ -265,18 +265,18 @@ describe('StsCredentials', () => { { authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, }, - additionalHeaders - ) + additionalHeaders, + ), ); const stsCredentials = new StsCredentials( tokenExchangeEndpoint, - basicAuth + basicAuth, ); const resp = await stsCredentials.exchangeToken( stsCredentialsOptions, additionalHeaders, - options + options, ); // Confirm raw GaxiosResponse appended to response. @@ -293,15 +293,15 @@ describe('StsCredentials', () => { expectedPartialRequest, { authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, - } + }, ); const stsCredentials = new StsCredentials( tokenExchangeEndpoint, - basicAuth + basicAuth, ); const resp = await stsCredentials.exchangeToken( - partialStsCredentialsOptions + partialStsCredentialsOptions, ); // Confirm raw GaxiosResponse appended to response. @@ -321,21 +321,21 @@ describe('StsCredentials', () => { { authorization: `Basic ${crypto.encodeBase64StringUtf8(creds)}`, }, - additionalHeaders - ) + additionalHeaders, + ), ); const stsCredentials = new StsCredentials( tokenExchangeEndpoint, - basicAuth + basicAuth, ); await assert.rejects( stsCredentials.exchangeToken( stsCredentialsOptions, additionalHeaders, - options + options, ), - expectedError + expectedError, ); scope.done(); }); @@ -347,17 +347,17 @@ describe('StsCredentials', () => { 200, stsSuccessfulResponse, expectedRequestWithCreds, - additionalHeaders + additionalHeaders, ); const stsCredentials = new StsCredentials( tokenExchangeEndpoint, - requestBodyAuth + requestBodyAuth, ); const resp = await stsCredentials.exchangeToken( stsCredentialsOptions, additionalHeaders, - options + options, ); // Confirm raw GaxiosResponse appended to response. @@ -371,15 +371,15 @@ describe('StsCredentials', () => { const scope = mockStsTokenExchange( 200, stsSuccessfulResponse, - expectedPartialRequestWithCreds + expectedPartialRequestWithCreds, ); const stsCredentials = new StsCredentials( tokenExchangeEndpoint, - requestBodyAuth + requestBodyAuth, ); const resp = await stsCredentials.exchangeToken( - partialStsCredentialsOptions + partialStsCredentialsOptions, ); // Confirm raw GaxiosResponse appended to response. @@ -395,20 +395,20 @@ describe('StsCredentials', () => { 400, errorResponse, expectedRequestWithCreds, - additionalHeaders + additionalHeaders, ); const stsCredentials = new StsCredentials( tokenExchangeEndpoint, - requestBodyAuth + requestBodyAuth, ); await assert.rejects( stsCredentials.exchangeToken( stsCredentialsOptions, additionalHeaders, - options + options, ), - expectedError + expectedError, ); scope.done(); }); diff --git a/tsconfig.json b/tsconfig.json index 6416b355..d3801278 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,17 @@ { "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { - "lib": ["es2018", "dom"], + "lib": ["DOM"], "rootDir": ".", - "outDir": "build", - "sourceMap": false + "outDir": "build" }, "include": [ - "src/*.ts", "src/**/*.ts", - "test/*.ts", + "src/**/*.cts", "test/**/*.ts", "system-test/*.ts", "browser-test/*.ts", "browser-test/**/*.ts" ], - "exclude": [ - "test/fixtures" - ] + "exclude": ["test/fixtures"] } From 13ca1dcad1f79e2015fa4326287caf9b43bc4cf2 Mon Sep 17 00:00:00 2001 From: Megan Potter <57276408+feywind@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:11:09 -0500 Subject: [PATCH 591/662] feat: add debug logging support (#1903) * feat: add debug logging support * build: add google-logging-utils in a temporary fashion * build: undo unintentional package.json changes * fix: move makeLog calls into shared classes for the most part * fix: update to anticipated release version of logger * fix: fix possible merging errors from earlier * fix: more potential merging issues * fix: more rearranging to match the older ordering * fix: merging two months of changes is sometimes trying * fix: very work in progress for switching to using interceptor and symbol based logs * fix: catch all exceptions, finish piping in method names * fix: various self-review updates * chore: put back missing comma * fix: update log name back to the original * fix: rejection interceptors must re-throw errors * chore: fix various reversions * chore: fix more linter issues * chore: remove extraneous makeLog * tests: add interceptor response shared tests * tests: add logging tests to interceptors * tests: lint fixes --- package.json | 1 + src/auth/authclient.ts | 116 +++++++++++ src/auth/baseexternalclient.ts | 7 +- .../defaultawssecuritycredentialssupplier.ts | 11 +- .../externalAccountAuthorizedUserClient.ts | 1 + src/auth/oauth2client.ts | 29 ++- src/auth/refreshclient.ts | 8 +- src/auth/stscredentials.ts | 4 +- src/auth/urlsubjecttokensupplier.ts | 4 + test/test.authclient.ts | 186 +++++++++++++++++- 10 files changed, 343 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 2fc27050..9cf1be64 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0-rc.4", "gcp-metadata": "^7.0.0-rc.1", + "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0-rc.1", "jws": "^4.0.0" }, diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 3c8f12b1..820f1495 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -17,9 +17,17 @@ import {Gaxios, GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; import {Credentials} from './credentials'; import {OriginalAndCamel, originalOrCamelOptions} from '../util'; +import {log as makeLog} from 'google-logging-utils'; import {PRODUCT_NAME, USER_AGENT} from '../shared.cjs'; +/** + * Easy access to symbol-indexed strings on config objects. + */ +export type SymbolIndexString = { + [key: symbol]: string | undefined; +}; + /** * Base auth configurations (e.g. from JWT or `.json` files) with conventional * camelCased options. @@ -210,6 +218,16 @@ export abstract class AuthClient forceRefreshOnFailure = false; universeDomain = DEFAULT_UNIVERSE; + /** + * Symbols that can be added to GaxiosOptions to specify the method name that is + * making an RPC call, for logging purposes, as well as a string ID that can be + * used to correlate calls and responses. + */ + static readonly RequestMethodNameSymbol: unique symbol = Symbol( + 'request method name', + ); + static readonly RequestLogIdSymbol: unique symbol = Symbol('request log id'); + constructor(opts: AuthClientOptions = {}) { super(); @@ -229,6 +247,9 @@ export abstract class AuthClient this.transporter.interceptors.request.add( AuthClient.DEFAULT_REQUEST_INTERCEPTOR, ); + this.transporter.interceptors.response.add( + AuthClient.DEFAULT_RESPONSE_INTERCEPTOR, + ); } if (opts.eagerRefreshThresholdMillis) { @@ -322,6 +343,7 @@ export abstract class AuthClient return target; } + static log = makeLog('auth'); static readonly DEFAULT_REQUEST_INTERCEPTOR: Parameters< Gaxios['interceptors']['request']['add'] >[0] = { @@ -340,10 +362,104 @@ export abstract class AuthClient config.headers.set('User-Agent', `${userAgent} ${USER_AGENT}`); } + try { + const symbols: SymbolIndexString = + config as unknown as SymbolIndexString; + const methodName = symbols[AuthClient.RequestMethodNameSymbol]; + + // This doesn't need to be very unique or interesting, it's just an aid for + // matching requests to responses. + const logId = `${Math.floor(Math.random() * 1000)}`; + symbols[AuthClient.RequestLogIdSymbol] = logId; + + // Boil down the object we're printing out. + const logObject = { + url: config.url, + headers: config.headers, + }; + if (methodName) { + AuthClient.log.info( + '%s [%s] request %j', + methodName, + logId, + logObject, + ); + } else { + AuthClient.log.info('[%s] request %j', logId, logObject); + } + } catch (e) { + // Logging must not create new errors; swallow them all. + } + return config; }, }; + static readonly DEFAULT_RESPONSE_INTERCEPTOR: Parameters< + Gaxios['interceptors']['response']['add'] + >[0] = { + resolved: async response => { + try { + const symbols: SymbolIndexString = + response.config as unknown as SymbolIndexString; + const methodName = symbols[AuthClient.RequestMethodNameSymbol]; + const logId = symbols[AuthClient.RequestLogIdSymbol]; + if (methodName) { + AuthClient.log.info( + '%s [%s] response %j', + methodName, + logId, + response.data, + ); + } else { + AuthClient.log.info('[%s] response %j', logId, response.data); + } + } catch (e) { + // Logging must not create new errors; swallow them all. + } + + return response; + }, + rejected: async error => { + try { + const symbols: SymbolIndexString = + error.config as unknown as SymbolIndexString; + const methodName = symbols[AuthClient.RequestMethodNameSymbol]; + const logId = symbols[AuthClient.RequestLogIdSymbol]; + if (methodName) { + AuthClient.log.info( + '%s [%s] error %j', + methodName, + logId, + error.response?.data, + ); + } else { + AuthClient.log.error('[%s] error %j', logId, error.response?.data); + } + } catch (e) { + // Logging must not create new errors; swallow them all. + } + + // Re-throw the error. + throw error; + }, + }; + + /** + * Sets the method name that is making a Gaxios request, so that logging may tag + * log lines with the operation. + * @param config A Gaxios request config + * @param methodName The method name making the call + */ + static setMethodName(config: GaxiosOptions, methodName: string) { + try { + const symbols: SymbolIndexString = config as unknown as SymbolIndexString; + symbols[AuthClient.RequestMethodNameSymbol] = methodName; + } catch (e) { + // Logging must not create new errors; swallow them all. + } + } + /** * Retry config for Auth-related requests. * diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 2c337583..b1e7d61f 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -472,11 +472,13 @@ export abstract class BaseExternalAccountClient extends AuthClient { } else if (projectNumber) { // Preferable not to use request() to avoid retrial policies. const headers = await this.getRequestHeaders(); - const response = await this.transporter.request({ + const opts: GaxiosOptions = { ...BaseExternalAccountClient.RETRY_CONFIG, headers, url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`, - }); + }; + AuthClient.setMethodName(opts, 'getProjectId'); + const response = await this.transporter.request(opts); this.projectId = response.data.projectId; return this.projectId; } @@ -667,6 +669,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { lifetime: this.serviceAccountImpersonationLifetime + 's', }, }; + AuthClient.setMethodName(opts, 'getImpersonatedAccessToken'); const response = await this.transporter.request(opts); const successResponse = response.data; diff --git a/src/auth/defaultawssecuritycredentialssupplier.ts b/src/auth/defaultawssecuritycredentialssupplier.ts index e215d1f0..a59818be 100644 --- a/src/auth/defaultawssecuritycredentialssupplier.ts +++ b/src/auth/defaultawssecuritycredentialssupplier.ts @@ -16,6 +16,7 @@ import {ExternalAccountSupplierContext} from './baseexternalclient'; import {Gaxios, GaxiosOptions} from 'gaxios'; import {AwsSecurityCredentialsSupplier} from './awsclient'; import {AwsSecurityCredentials} from './awsrequestsigner'; +import {AuthClient} from './authclient'; /** * Interface defining the AWS security-credentials endpoint response. @@ -128,6 +129,7 @@ export class DefaultAwsSecurityCredentialsSupplier method: 'GET', headers: metadataHeaders, }; + AuthClient.setMethodName(opts, 'getAwsRegion'); const response = await context.transporter.request(opts); // Remove last character. For example, if us-east-2b is returned, // the region would be us-east-2. @@ -191,6 +193,7 @@ export class DefaultAwsSecurityCredentialsSupplier method: 'PUT', headers: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }; + AuthClient.setMethodName(opts, '#getImdsV2SessionToken'); const response = await transporter.request(opts); return response.data; } @@ -217,6 +220,7 @@ export class DefaultAwsSecurityCredentialsSupplier method: 'GET', headers: headers, }; + AuthClient.setMethodName(opts, '#getAwsRoleName'); const response = await transporter.request(opts); return response.data; } @@ -235,11 +239,14 @@ export class DefaultAwsSecurityCredentialsSupplier headers: Headers, transporter: Gaxios, ): Promise { - const response = await transporter.request({ + const opts = { ...this.additionalGaxiosOptions, url: `${this.securityCredentialsUrl}/${roleName}`, headers: headers, - }); + } as GaxiosOptions; + AuthClient.setMethodName(opts, '#retrieveAwsSecurityCredentials'); + const response = + await transporter.request(opts); return response.data; } diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index 93c33ef6..d473e4dc 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -121,6 +121,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { refresh_token: refreshToken, }), }; + AuthClient.setMethodName(opts, 'refreshToken'); // Apply OAuth client authentication. this.applyClientAuthenticationOptions(opts); diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 5e2302ec..2c70a8ce 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -749,13 +749,16 @@ export class OAuth2Client extends AuthClient { if (this.clientAuthentication === ClientAuthentication.ClientSecretPost) { values.client_secret = this._clientSecret; } - const res = await this.transporter.request({ + + const opts = { ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, data: new URLSearchParams(values as {}), headers, - }); + }; + AuthClient.setMethodName(opts, 'getTokenAsync'); + const res = await this.transporter.request(opts); const tokens = res.data as Credentials; if (res.data && res.data.expires_in) { tokens.expiry_date = new Date().getTime() + res.data.expires_in * 1000; @@ -813,13 +816,16 @@ export class OAuth2Client extends AuthClient { let res: GaxiosResponse; try { - // request for new token - res = await this.transporter.request({ + const opts: GaxiosOptions = { ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, data: new URLSearchParams(data), - }); + }; + AuthClient.setMethodName(opts, 'refreshTokenNoCache'); + + // request for new token + res = await this.transporter.request(opts); } catch (e) { if ( e instanceof GaxiosError && @@ -1041,6 +1047,7 @@ export class OAuth2Client extends AuthClient { url: this.getRevokeTokenURL(token).toString(), method: 'POST', }; + AuthClient.setMethodName(opts, 'revokeToken'); if (callback) { this.transporter .request(opts) @@ -1306,10 +1313,12 @@ export class OAuth2Client extends AuthClient { throw new Error(`Unsupported certificate format ${format}`); } try { - res = await this.transporter.request({ + const opts = { ...OAuth2Client.RETRY_CONFIG, url, - }); + }; + AuthClient.setMethodName(opts, 'getFederatedSignonCertsAsync'); + res = await this.transporter.request(opts); } catch (e) { if (e instanceof Error) { e.message = `Failed to retrieve verification certificates: ${e.message}`; @@ -1377,10 +1386,12 @@ export class OAuth2Client extends AuthClient { const url = this.endpoints.oauth2IapPublicKeyUrl.toString(); try { - res = await this.transporter.request({ + const opts = { ...OAuth2Client.RETRY_CONFIG, url, - }); + }; + AuthClient.setMethodName(opts, 'getIapPublicKeysAsync'); + res = await this.transporter.request(opts); } catch (e) { if (e instanceof Error) { e.message = `Failed to retrieve verification certificates: ${e.message}`; diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index e9f3387b..41abcaea 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -19,6 +19,8 @@ import { OAuth2Client, OAuth2ClientOptions, } from './oauth2client'; +import {AuthClient} from './authclient'; +import {GaxiosOptions} from 'gaxios'; export const USER_REFRESH_ACCOUNT_TYPE = 'authorized_user'; @@ -96,7 +98,7 @@ export class UserRefreshClient extends OAuth2Client { } async fetchIdToken(targetAudience: string): Promise { - const res = await this.transporter.request({ + const opts: GaxiosOptions = { ...UserRefreshClient.RETRY_CONFIG, url: this.endpoints.oauth2TokenUrl, method: 'POST', @@ -107,8 +109,10 @@ export class UserRefreshClient extends OAuth2Client { refresh_token: this._refreshToken, target_audience: targetAudience, } as {}), - }); + }; + AuthClient.setMethodName(opts, 'fetchIdToken'); + const res = await this.transporter.request(opts); return res.data.id_token!; } diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 6b1c91c9..65175f1f 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -13,7 +13,7 @@ // limitations under the License. import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios'; -import {HeadersInit} from './authclient'; +import {AuthClient, HeadersInit} from './authclient'; import { ClientAuthentication, OAuthClientAuthHandler, @@ -218,6 +218,8 @@ export class StsCredentials extends OAuthClientAuthHandler { headers, data: new URLSearchParams(payload), }; + AuthClient.setMethodName(opts, 'exchangeToken'); + // Apply OAuth client authentication. this.applyClientAuthenticationOptions(opts); diff --git a/src/auth/urlsubjecttokensupplier.ts b/src/auth/urlsubjecttokensupplier.ts index 1d41089e..75a0f45f 100644 --- a/src/auth/urlsubjecttokensupplier.ts +++ b/src/auth/urlsubjecttokensupplier.ts @@ -19,6 +19,7 @@ import { SubjectTokenJsonResponse, SubjectTokenSupplier, } from './identitypoolclient'; +import {AuthClient} from './authclient'; /** * Interface that defines options used to build a {@link UrlSubjectTokenSupplier} @@ -88,6 +89,8 @@ export class UrlSubjectTokenSupplier implements SubjectTokenSupplier { method: 'GET', headers: this.headers, }; + AuthClient.setMethodName(opts, 'getSubjectToken'); + let subjectToken: string | undefined; if (this.formatType === 'text') { const response = await context.transporter.request(opts); @@ -102,6 +105,7 @@ export class UrlSubjectTokenSupplier implements SubjectTokenSupplier { 'Unable to parse the subject_token from the credential_source URL', ); } + return subjectToken; } } diff --git a/test/test.authclient.ts b/test/test.authclient.ts index f82d8644..235bfc9b 100644 --- a/test/test.authclient.ts +++ b/test/test.authclient.ts @@ -14,11 +14,41 @@ import {strict as assert} from 'assert'; -import {Gaxios, GaxiosOptionsPrepared} from 'gaxios'; +import { + Gaxios, + GaxiosError, + GaxiosOptionsPrepared, + GaxiosResponse, +} from 'gaxios'; import {AuthClient, PassThroughClient} from '../src'; import {snakeToCamel} from '../src/util'; import {PRODUCT_NAME, USER_AGENT} from '../src/shared.cjs'; +import * as logging from 'google-logging-utils'; + +// Fakes for the logger, to capture logs that would've happened. +interface TestLog { + namespace: string; + fields: logging.LogFields; + args: unknown[]; +} + +class TestLogSink extends logging.DebugLogBackendBase { + logs: TestLog[] = []; + + makeLogger(namespace: string): logging.AdhocDebugLogCallable { + return (fields: logging.LogFields, ...args: unknown[]) => { + this.logs.push({namespace, fields, args}); + }; + } + + setFilters(): void {} + + reset() { + this.filters = []; + this.logs = []; + } +} describe('AuthClient', () => { it('should accept and normalize snake case options to camel case', () => { @@ -42,8 +72,8 @@ describe('AuthClient', () => { } }); - describe('shared auth interceptor', () => { - it('should use the default interceptor', () => { + describe('shared auth interceptors', () => { + it('should use the default interceptors', () => { const gaxios = new Gaxios(); new PassThroughClient({transporter: gaxios}); @@ -51,11 +81,18 @@ describe('AuthClient', () => { assert( gaxios.interceptors.request.has(AuthClient.DEFAULT_REQUEST_INTERCEPTOR), ); + assert( + gaxios.interceptors.response.has( + AuthClient.DEFAULT_RESPONSE_INTERCEPTOR, + ), + ); }); it('should allow disabling of the default interceptor', () => { const gaxios = new Gaxios(); - const originalInterceptorCount = gaxios.interceptors.request.size; + const originalRequestInterceptorCount = gaxios.interceptors.request.size; + const originalResponseInterceptorCount = + gaxios.interceptors.response.size; const authClient = new PassThroughClient({ transporter: gaxios, @@ -65,19 +102,35 @@ describe('AuthClient', () => { assert.equal(authClient.transporter, gaxios); assert.equal( authClient.transporter.interceptors.request.size, - originalInterceptorCount, + originalRequestInterceptorCount, + ); + assert.equal( + authClient.transporter.interceptors.response.size, + originalResponseInterceptorCount, ); }); it('should add the default interceptor exactly once between instances', () => { const gaxios = new Gaxios(); - const originalInterceptorCount = gaxios.interceptors.request.size; - const expectedInterceptorCount = originalInterceptorCount + 1; + const originalRequestInterceptorCount = gaxios.interceptors.request.size; + const expectedRequestInterceptorCount = + originalRequestInterceptorCount + 1; + const originalResponseInterceptorCount = + gaxios.interceptors.response.size; + const expectedResponseInterceptorCount = + originalResponseInterceptorCount + 1; new PassThroughClient({transporter: gaxios}); new PassThroughClient({transporter: gaxios}); - assert.equal(gaxios.interceptors.request.size, expectedInterceptorCount); + assert.equal( + gaxios.interceptors.request.size, + expectedRequestInterceptorCount, + ); + assert.equal( + gaxios.interceptors.response.size, + expectedResponseInterceptorCount, + ); }); describe('User-Agent', () => { @@ -151,5 +204,122 @@ describe('AuthClient', () => { assert.equal(options.headers.get('x-goog-api-client'), expected); }); }); + + describe('logging', () => { + // Enable and capture any log lines that happen during these tests. + let testLogSink: TestLogSink; + let replacementLogger: logging.AdhocDebugLogFunction; + beforeEach(() => { + process.env[logging.env.nodeEnables] = 'auth'; + testLogSink = new TestLogSink(); + logging.setBackend(testLogSink); + replacementLogger = logging.log('auth'); + }); + after(() => { + delete process.env[logging.env.nodeEnables]; + logging.setBackend(null); + }); + + it('logs requests', async () => { + const options: GaxiosOptionsPrepared = { + headers: new Headers({ + 'x-goog-api-client': 'something', + }), + url: new URL('https://google.com'), + }; + AuthClient.setMethodName(options, 'testMethod'); + + // This will become nicer with the 1.1.0 release of google-logging-utils. + AuthClient.log = replacementLogger; + const returned = + await AuthClient.DEFAULT_REQUEST_INTERCEPTOR?.resolved?.(options); + assert.strictEqual(returned, options); + + // Unfortunately, there is a fair amount of entropy and changeable formatting in the + // actual logs, so this mostly validates that a few key pieces of info are in there. + assert.deepStrictEqual(testLogSink.logs.length, 1); + assert.deepStrictEqual(testLogSink.logs[0].namespace, 'auth'); + assert.deepStrictEqual(testLogSink.logs[0].args.length, 4); + assert.strictEqual( + (testLogSink.logs[0].args[0] as string).includes('request'), + true, + ); + assert.deepStrictEqual(testLogSink.logs[0].args[1], 'testMethod'); + assert.deepStrictEqual( + (testLogSink.logs[0].args[3] as GaxiosOptionsPrepared).headers.get( + 'x-goog-api-client', + ), + 'something', + ); + assert.deepStrictEqual( + (testLogSink.logs[0].args[3] as GaxiosOptionsPrepared).url.href, + 'https://google.com/', + ); + }); + + it('logs responses', async () => { + const response = { + config: { + headers: new Headers({ + 'x-goog-api-client': 'something', + }), + url: new URL('https://google.com'), + } as GaxiosOptionsPrepared, + headers: new Headers({ + 'x-goog-api-client': 'something', + }), + url: new URL('https://google.com'), + data: { + test: 'test!', + }, + } as unknown as GaxiosResponse<{test: string}>; + AuthClient.setMethodName(response.config, 'testMethod'); + + // This will become nicer with the 1.1.0 release of google-logging-utils. + AuthClient.log = replacementLogger; + const resolvedReturned = + await AuthClient.DEFAULT_RESPONSE_INTERCEPTOR?.resolved?.(response); + assert.strictEqual(resolvedReturned, response); + + // Unfortunately, there is a fair amount of entropy and changeable formatting in the + // actual logs, so this mostly validates that a few key pieces of info are in there. + assert.deepStrictEqual(testLogSink.logs.length, 1); + assert.deepStrictEqual(testLogSink.logs[0].namespace, 'auth'); + assert.deepStrictEqual(testLogSink.logs[0].args.length, 4); + assert.strictEqual( + (testLogSink.logs[0].args[0] as string).includes('response'), + true, + ); + assert.deepStrictEqual(testLogSink.logs[0].args[1], 'testMethod'); + assert.deepStrictEqual(testLogSink.logs[0].args[3] as {test: string}, { + test: 'test!', + }); + + const error = { + config: response.config, + response: { + data: { + message: 'boo!', + }, + }, + } as unknown as GaxiosError<{test: string}>; + testLogSink.reset(); + AuthClient.DEFAULT_RESPONSE_INTERCEPTOR?.rejected?.(error); + + // Unfortunately, there is a fair amount of entropy and changeable formatting in the + // actual logs, so this mostly validates that a few key pieces of info are in there. + assert.deepStrictEqual(testLogSink.logs.length, 1); + assert.deepStrictEqual(testLogSink.logs[0].namespace, 'auth'); + assert.deepStrictEqual(testLogSink.logs[0].args.length, 4); + assert.strictEqual( + (testLogSink.logs[0].args[0] as string).includes('error'), + true, + ); + assert.deepStrictEqual(testLogSink.logs[0].args[1], 'testMethod'); + assert.deepStrictEqual(testLogSink.logs[0].args[3] as {test: string}, { + message: 'boo!', + }); + }); + }); }); }); From c5cf5548dce7bb06e591c1a5cb49a07f1e0ee26c Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:39:12 -0700 Subject: [PATCH 592/662] chore: update owlbot-nodejs dependencies (#1956) * chore: update owlbot-nodejs dependencies * Update container_test.yaml Source-Link: https://github.com/googleapis/synthtool/commit/1e798e6de27c63a88a1768c2a5f73b85e1523a21 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:c7e4968cfc97a204a4b2381f3ecb55cabc40c4cccf88b1ef8bef0d976be87fee Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 6 +- .github/PULL_REQUEST_TEMPLATE.md | 33 +++++- .github/scripts/close-invalid-link.cjs | 41 +++++-- .github/scripts/close-unresponsive.cjs | 108 ++++++++--------- .github/scripts/fixtures/invalidIssueBody.txt | 50 ++++++++ .github/scripts/fixtures/validIssueBody.txt | 50 ++++++++ .../validIssueBodyDifferentLinkLocation.txt | 50 ++++++++ .github/scripts/package.json | 21 ++++ .github/scripts/remove-response-label.cjs | 28 ++--- .../scripts/tests/close-invalid-link.test.cjs | 86 ++++++++++++++ .../close-or-remove-response-label.test.cjs | 109 ++++++++++++++++++ .github/workflows/issues-no-repro.yaml | 5 + 12 files changed, 499 insertions(+), 88 deletions(-) create mode 100644 .github/scripts/fixtures/invalidIssueBody.txt create mode 100644 .github/scripts/fixtures/validIssueBody.txt create mode 100644 .github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt create mode 100644 .github/scripts/package.json create mode 100644 .github/scripts/tests/close-invalid-link.test.cjs create mode 100644 .github/scripts/tests/close-or-remove-response-label.test.cjs diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 39a62ca6..60443342 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# 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. @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:0d39e59663287ae929c1d4ccf8ebf7cef9946826c9b86eda7e85d8d752dbb584 -# created: 2024-11-21T22:39:44.342569463Z + digest: sha256:c7e4968cfc97a204a4b2381f3ecb55cabc40c4cccf88b1ef8bef0d976be87fee +# created: 2025-04-08T17:33:08.498793944Z diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6326e141..fe9d61e7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,30 @@ -Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: -- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/google-auth-library-nodejs/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +> Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: + +## Description + +> Please provide a detailed description for the change. +> As much as possible, please try to keep changes separate by purpose. For example, try not to make a one-line bug fix in a feature request, or add an irrelevant README change to a bug fix. + +## Impact + +> What's the impact of this change? + +## Testing + +> Have you added unit and integration tests if necessary? +> Were any tests changed? Are any breaking changes necessary? + +## Additional Information + +> Any additional details that we should be aware of? + +## Checklist + +- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/google-auth-library-nodejs/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass -- [ ] Code coverage does not decrease (if any source code was changed) -- [ ] Appropriate docs were updated (if necessary) +- [ ] Code coverage does not decrease +- [ ] Appropriate docs were updated +- [ ] Appropriate comments were added, particularly in complex areas or places that require background +- [ ] No new warnings or issues will be generated from this change -Fixes # 🦕 +Fixes #issue_number_goes_here 🦕 diff --git a/.github/scripts/close-invalid-link.cjs b/.github/scripts/close-invalid-link.cjs index d7a3688e..fdb51488 100644 --- a/.github/scripts/close-invalid-link.cjs +++ b/.github/scripts/close-invalid-link.cjs @@ -12,21 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. +const fs = require('fs'); +const yaml = require('js-yaml'); +const path = require('path'); +const TEMPLATE_FILE_PATH = path.resolve(__dirname, '../ISSUE_TEMPLATE/bug_report.yml') + async function closeIssue(github, owner, repo, number) { await github.rest.issues.createComment({ owner: owner, repo: repo, issue_number: number, - body: 'Issue was opened with an invalid reproduction link. Please make sure the repository is a valid, publicly-accessible github repository, and make sure the url is complete (example: https://github.com/googleapis/google-cloud-node)' + body: "Issue was opened with an invalid reproduction link. Please make sure the repository is a valid, publicly-accessible github repository, and make sure the url is complete (example: https://github.com/googleapis/google-cloud-node)" }); await github.rest.issues.update({ owner: owner, repo: repo, issue_number: number, - state: 'closed' + state: "closed" }); } -module.exports = async ({github, context}) => { +module.exports = async ({ github, context }) => { const owner = context.repo.owner; const repo = context.repo.repo; const number = context.issue.number; @@ -37,20 +42,32 @@ module.exports = async ({github, context}) => { issue_number: number, }); - const isBugTemplate = issue.data.body.includes('Link to the code that reproduces this issue'); + const yamlData = fs.readFileSync(TEMPLATE_FILE_PATH, 'utf8'); + const obj = yaml.load(yamlData); + const linkMatchingText = (obj.body.find(x => {return x.type === 'input' && x.validations.required === true && x.attributes.label.includes('link')})).attributes.label; + const isBugTemplate = issue.data.body.includes(linkMatchingText); if (isBugTemplate) { console.log(`Issue ${number} is a bug template`) try { - const link = issue.data.body.split('\n')[18].match(/(https?:\/\/(gist\.)?github.com\/.*)/)[0]; - console.log(`Issue ${number} contains this link: ${link}`) - const isValidLink = (await fetch(link)).ok; - console.log(`Issue ${number} has a ${isValidLink ? 'valid' : 'invalid'} link`) - if (!isValidLink) { - await closeIssue(github, owner, repo, number); - } + const text = issue.data.body; + const match = text.indexOf(linkMatchingText); + if (match !== -1) { + const nextLineIndex = text.indexOf('http', match); + if (nextLineIndex == -1) { + await closeIssue(github, owner, repo, number); + return; + } + const link = text.substring(nextLineIndex, text.indexOf('\n', nextLineIndex)); + console.log(`Issue ${number} contains this link: ${link}`); + const isValidLink = (await fetch(link)).ok; + console.log(`Issue ${number} has a ${isValidLink ? "valid" : "invalid"} link`) + if (!isValidLink) { + await closeIssue(github, owner, repo, number); + } + } } catch (err) { await closeIssue(github, owner, repo, number); } } -}; +}; \ No newline at end of file diff --git a/.github/scripts/close-unresponsive.cjs b/.github/scripts/close-unresponsive.cjs index 142dc126..6f81b508 100644 --- a/.github/scripts/close-unresponsive.cjs +++ b/.github/scripts/close-unresponsive.cjs @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +/// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,57 +13,57 @@ // limitations under the License. function labeledEvent(data) { - return data.event === 'labeled' && data.label.name === 'needs more info'; - } - - const numberOfDaysLimit = 15; - const close_message = `This has been closed since a request for information has \ - not been answered for ${numberOfDaysLimit} days. It can be reopened when the \ - requested information is provided.`; - - module.exports = async ({github, context}) => { - const owner = context.repo.owner; - const repo = context.repo.repo; - - const issues = await github.rest.issues.listForRepo({ - owner: owner, - repo: repo, - labels: 'needs more info', - }); - const numbers = issues.data.map((e) => e.number); - - for (const number of numbers) { - const events = await github.paginate( - github.rest.issues.listEventsForTimeline, - { - owner: owner, - repo: repo, - issue_number: number, - }, - (response) => response.data.filter(labeledEvent) - ); - - const latest_response_label = events[events.length - 1]; - - const created_at = new Date(latest_response_label.created_at); - const now = new Date(); - const diff = now - created_at; - const diffDays = diff / (1000 * 60 * 60 * 24); - - if (diffDays > numberOfDaysLimit) { - await github.rest.issues.update({ - owner: owner, - repo: repo, - issue_number: number, - state: 'closed', - }); - - await github.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: number, - body: close_message, - }); - } + return data.event === "labeled" && data.label.name === "needs more info"; +} + +const numberOfDaysLimit = 15; +const close_message = `This has been closed since a request for information has \ +not been answered for ${numberOfDaysLimit} days. It can be reopened when the \ +requested information is provided.`; + +module.exports = async ({ github, context }) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + + const issues = await github.rest.issues.listForRepo({ + owner: owner, + repo: repo, + labels: "needs more info", + }); + const numbers = issues.data.map((e) => e.number); + + for (const number of numbers) { + const events = await github.paginate( + github.rest.issues.listEventsForTimeline, + { + owner: owner, + repo: repo, + issue_number: number, + }, + (response) => response.data.filter(labeledEvent) + ); + + const latest_response_label = events[events.length - 1]; + + const created_at = new Date(latest_response_label.created_at); + const now = new Date(); + const diff = now - created_at; + const diffDays = diff / (1000 * 60 * 60 * 24); + + if (diffDays > numberOfDaysLimit) { + await github.rest.issues.update({ + owner: owner, + repo: repo, + issue_number: number, + state: "closed", + }); + + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: number, + body: close_message, + }); } - }; + } +}; \ No newline at end of file diff --git a/.github/scripts/fixtures/invalidIssueBody.txt b/.github/scripts/fixtures/invalidIssueBody.txt new file mode 100644 index 00000000..504bd669 --- /dev/null +++ b/.github/scripts/fixtures/invalidIssueBody.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### Link to the code that reproduces this issue. A link to a **public** Github Repository or gist with a minimal reproduction. + +not-a-link + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/fixtures/validIssueBody.txt b/.github/scripts/fixtures/validIssueBody.txt new file mode 100644 index 00000000..6e0ace33 --- /dev/null +++ b/.github/scripts/fixtures/validIssueBody.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### Link to the code that reproduces this issue. A link to a **public** Github Repository or gist with a minimal reproduction. + +https://gist.github.com/orgads/13cbf44c91923da27d8772b5f10489c9 + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt b/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt new file mode 100644 index 00000000..984a420e --- /dev/null +++ b/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + +### Link to the code that reproduces this issue. A link to a **public** Github Repository with a minimal reproduction. + + +https://gist.github.com/orgads/13cbf44c91923da27d8772b5f10489c9 + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/package.json b/.github/scripts/package.json new file mode 100644 index 00000000..2c2e5207 --- /dev/null +++ b/.github/scripts/package.json @@ -0,0 +1,21 @@ +{ + "name": "tests", + "private": true, + "description": "tests for script", + "scripts": { + "test": "mocha tests/close-invalid-link.test.cjs && mocha tests/close-or-remove-response-label.test.cjs" + }, + "author": "Google Inc.", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@octokit/rest": "^19.0.0", + "mocha": "^10.0.0", + "sinon": "^18.0.0" + } +} \ No newline at end of file diff --git a/.github/scripts/remove-response-label.cjs b/.github/scripts/remove-response-label.cjs index 887cf349..4a784ddf 100644 --- a/.github/scripts/remove-response-label.cjs +++ b/.github/scripts/remove-response-label.cjs @@ -13,21 +13,21 @@ // limitations under the License. module.exports = async ({ github, context }) => { - const commenter = context.actor; - const issue = await github.rest.issues.get({ + const commenter = context.actor; + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const author = issue.data.user.login; + const labels = issue.data.labels.map((e) => e.name); + + if (author === commenter && labels.includes("needs more info")) { + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, + name: "needs more info", }); - const author = issue.data.user.login; - const labels = issue.data.labels.map((e) => e.name); - - if (author === commenter && labels.includes('needs more info')) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: 'needs more info', - }); - } - }; + } +}; \ No newline at end of file diff --git a/.github/scripts/tests/close-invalid-link.test.cjs b/.github/scripts/tests/close-invalid-link.test.cjs new file mode 100644 index 00000000..f63ee89c --- /dev/null +++ b/.github/scripts/tests/close-invalid-link.test.cjs @@ -0,0 +1,86 @@ +// Copyright 2024 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 +// +// https://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. + +'use strict'; + +const { describe, it } = require('mocha'); +const closeInvalidLink = require('../close-invalid-link.cjs'); +const fs = require('fs'); +const sinon = require('sinon'); + +describe('close issues with invalid links', () => { + let octokitStub; + let issuesStub; + + beforeEach(() => { + issuesStub = { + get: sinon.stub(), + createComment: sinon.stub(), + update: sinon.stub(), + }; + octokitStub = { + rest: { + issues: issuesStub, + }, + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('does not do anything if it is not a bug', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: "I'm having a problem with this." } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + it('does not do anything if it is a bug with an appropriate link', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/validIssueBody.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + it('does not do anything if it is a bug with an appropriate link and the template changes', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/validIssueBodyDifferentLinkLocation.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + it('closes the issue if the link is invalid', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/invalidIssueBody.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.calledOnce(issuesStub.createComment); + sinon.assert.calledOnce(issuesStub.update); + }); +}); \ No newline at end of file diff --git a/.github/scripts/tests/close-or-remove-response-label.test.cjs b/.github/scripts/tests/close-or-remove-response-label.test.cjs new file mode 100644 index 00000000..fb092c53 --- /dev/null +++ b/.github/scripts/tests/close-or-remove-response-label.test.cjs @@ -0,0 +1,109 @@ +// Copyright 2024 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 +// +// https://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. + +'use strict'; + +const { describe, it, beforeEach, afterEach } = require('mocha'); +const removeResponseLabel = require('../remove-response-label.cjs'); +const closeUnresponsive = require('../close-unresponsive.cjs'); +const sinon = require('sinon'); + +function getISODateDaysAgo(days) { + const today = new Date(); + const daysAgo = new Date(today.setDate(today.getDate() - days)); + return daysAgo.toISOString(); +} + +describe('close issues or remove needs more info labels', () => { + let octokitStub; + let issuesStub; + let paginateStub; + + beforeEach(() => { + issuesStub = { + listForRepo: sinon.stub(), + update: sinon.stub(), + createComment: sinon.stub(), + get: sinon.stub(), + removeLabel: sinon.stub(), + }; + paginateStub = sinon.stub(); + octokitStub = { + rest: { + issues: issuesStub, + }, + paginate: paginateStub, + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('closes the issue if the OP has not responded within the allotted time and there is a needs-more-info label', async () => { + const context = { owner: 'testOrg', repo: 'testRepo' }; + const issuesInRepo = [{ user: { login: 'OP' }, labels: [{ name: 'needs more info' }] }]; + const eventsInIssue = [{ event: 'labeled', label: { name: 'needs more info' }, created_at: getISODateDaysAgo(16) }]; + + issuesStub.listForRepo.resolves({ data: issuesInRepo }); + paginateStub.resolves(eventsInIssue); + + await closeUnresponsive({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.listForRepo); + sinon.assert.calledOnce(paginateStub); + sinon.assert.calledOnce(issuesStub.update); + sinon.assert.calledOnce(issuesStub.createComment); + }); + + it('does nothing if not enough time has passed and there is a needs-more-info label', async () => { + const context = { owner: 'testOrg', repo: 'testRepo' }; + const issuesInRepo = [{ user: { login: 'OP' }, labels: [{ name: 'needs more info' }] }]; + const eventsInIssue = [{ event: 'labeled', label: { name: 'needs more info' }, created_at: getISODateDaysAgo(14) }]; + + issuesStub.listForRepo.resolves({ data: issuesInRepo }); + paginateStub.resolves(eventsInIssue); + + await closeUnresponsive({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.listForRepo); + sinon.assert.calledOnce(paginateStub); + sinon.assert.notCalled(issuesStub.update); + sinon.assert.notCalled(issuesStub.createComment); + }); + + it('removes the label if OP responded', async () => { + const context = { actor: 'OP', repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + const issueContext = { user: {login: 'OP'}, labels: [{ name: 'needs more info' }] }; + + issuesStub.get.resolves({ data: issueContext }); + + await removeResponseLabel({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.calledOnce(issuesStub.removeLabel); + }); + + it('does not remove the label if author responded', async () => { + const context = { actor: 'repo-maintainer', repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + const issueContext = { user: {login: 'OP'}, labels: [{ name: 'needs more info' }] }; + + issuesStub.get.resolves({ data: issueContext }); + + await removeResponseLabel({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.removeLabel); + }); +}); \ No newline at end of file diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 442a46bc..9b2f7014 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,6 +11,11 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - run: npm install + working-directory: ./.github/scripts - uses: actions/github-script@v7 with: script: | From 629da74095ae51c258de13d924b65bb3c31ddf82 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 23 Apr 2025 19:48:48 +0100 Subject: [PATCH 593/662] fix(deps): update dependency @googleapis/iam to v27 (#1962) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 02fdde5d..78762231 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/language": "^6.5.0", "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^21.0.0", + "@googleapis/iam": "^27.0.0", "google-auth-library": "^9.15.1", "node-fetch": "^2.3.0", "open": "^9.0.0", From 813a63aadfbeb1d136b345b927431c1473bb3fe6 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Thu, 8 May 2025 07:11:10 -0700 Subject: [PATCH 594/662] docs: fix `jsdoc` and `linkinator` (#1965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: fix `jsoc` * chore: fix docs includes * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: minor clean-up * docs: Include documentation for `AuthClient` so it can be rendered * fix: add retries for 429s https://github.com/JustinBeckwith/linkinator-action?tab=readme-ov-file#inputs * chore: more linkinator * chore: sanity check * chore: remove concurrency * chore: more clean-up * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * docs: skip blobs --------- Co-authored-by: Owl Bot --- .github/workflows/ci.yaml | 35 ++++++++++++++++++++++++++++------- .jsdoc.js | 24 +++++++++--------------- linkinator.config.json | 8 ++++++-- owlbot.py | 1 - package.json | 2 +- src/auth/authclient.ts | 3 +++ 6 files changed, 47 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a67c3ec2..883082c0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [18, 20, 22] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -22,25 +22,39 @@ jobs: - run: npm install --production --engine-strict --ignore-scripts --no-package-lock # Clean up the production install, before installing dev/production: - run: rm -rf node_modules - - run: npm install + - run: npm install --engine-strict + - run: npm test + env: + MOCHA_THROW_DEPRECATION: false + test-script: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: node --version + - run: npm install --engine-strict + working-directory: .github/scripts - run: npm test + working-directory: .github/scripts env: MOCHA_THROW_DEPRECATION: false windows: runs-on: windows-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 - - run: npm install + - run: npm install --engine-strict - run: npm test env: MOCHA_THROW_DEPRECATION: false lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 @@ -49,5 +63,12 @@ jobs: docs: runs-on: ubuntu-latest steps: - # See: https://github.com/JustinBeckwith/linkinator-action/issues/148 - - run: echo "temporarily skipping until linkinator's fixed upstream" + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: npm install + - run: npm run docs + - uses: JustinBeckwith/linkinator-action@v1 + with: + paths: docs/ diff --git a/.jsdoc.js b/.jsdoc.js index a521abe2..6527e27d 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -22,30 +22,24 @@ module.exports = { template: './node_modules/jsdoc-fresh', recurse: true, verbose: true, - destination: './docs/' + destination: './docs/', }, - plugins: [ - 'plugins/markdown', - 'jsdoc-region-tag' - ], + plugins: ['plugins/markdown', 'jsdoc-region-tag'], source: { - excludePattern: '(^|\\/|\\\\)[._]', - include: [ - 'build/src', - ], - includePattern: '\\.js$' + include: ['build/src'], + includePattern: '\\.js$', }, templates: { - copyright: 'Copyright 2019 Google, LLC.', + copyright: 'Copyright 2019 Google LLC', includeDate: false, sourceFiles: false, systemName: 'google-auth-library', theme: 'lumen', default: { - outputSourceFiles: false - } + outputSourceFiles: false, + }, }, markdown: { - idInHeadings: true - } + idInHeadings: true, + }, }; diff --git a/linkinator.config.json b/linkinator.config.json index 48cfef38..d23de4eb 100644 --- a/linkinator.config.json +++ b/linkinator.config.json @@ -4,8 +4,12 @@ "https://codecov.io/gh/googleapis/", "www.googleapis.com", "img.shields.io", - "http://169.254.169.254/latest/api/token%22" + "http://169.254.169.254/latest/api/token%22", + "https://github.com/googleapis/google-auth-library-nodejs/blob/" ], "silent": true, - "concurrency": 10 + "retry": true, + "retryErrors": true, + "retryErrorsCount": 3, + "retryErrorsJitter": 5 } diff --git a/owlbot.py b/owlbot.py index d41bc272..4f417c8a 100644 --- a/owlbot.py +++ b/owlbot.py @@ -21,7 +21,6 @@ # List of excludes for the enhanced library node.owlbot_main( templates_excludes=[ - ".github/workflows/ci.yaml", ".github/ISSUE_TEMPLATE/bug_report.yml" ], ) diff --git a/package.json b/package.json index 9cf1be64..2eccce7f 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "compile": "tsc -p .", "fix": "gts fix", "pretest": "npm run compile -- --sourceMap", - "docs": "jsdoc -c .jsdoc.json", + "docs": "jsdoc -c .jsdoc.js", "samples-setup": "cd samples/ && npm link ../ && npm run setup && cd ../", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "system-test": "mocha build/system-test --timeout 60000", diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 820f1495..5f47200d 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -198,6 +198,9 @@ export declare interface AuthClient { on(event: 'tokens', listener: (tokens: Credentials) => void): this; } +/** + * The base of all Auth Clients. + */ export abstract class AuthClient extends EventEmitter implements CredentialsClient From 3b5351bb1ea3f0b34faca3bfbb9fe750c6c98f98 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 24 May 2025 07:14:21 +0200 Subject: [PATCH 595/662] chore(deps): update dependency @octokit/rest to v21 (#1958) --- .github/scripts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 2c2e5207..baad0f8f 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -14,7 +14,7 @@ "js-yaml": "^4.1.0" }, "devDependencies": { - "@octokit/rest": "^19.0.0", + "@octokit/rest": "^21.0.0", "mocha": "^10.0.0", "sinon": "^18.0.0" } From ea953e4957ae89ebbcbe38418530a314ffc606ec Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 24 May 2025 07:18:20 +0200 Subject: [PATCH 596/662] chore(deps): update actions/setup-node action to v4 (#1957) Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- .github/workflows/issues-no-repro.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 9b2f7014..816d9a70 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,7 +11,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install From 79d515907d3b9a62a4b2b5fa8469fcbea4059034 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 24 May 2025 07:19:08 +0200 Subject: [PATCH 597/662] chore(deps): update dependency sinon to v20 (#1953) Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- .github/scripts/package.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/package.json b/.github/scripts/package.json index baad0f8f..9d9fc372 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -16,6 +16,6 @@ "devDependencies": { "@octokit/rest": "^21.0.0", "mocha": "^10.0.0", - "sinon": "^18.0.0" + "sinon": "^20.0.0" } } \ No newline at end of file diff --git a/package.json b/package.json index 2eccce7f..783a43d7 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "nock": "^14.0.1", "null-loader": "^4.0.0", "puppeteer": "^24.0.0", - "sinon": "^18.0.1", + "sinon": "^20.0.0", "ts-loader": "^8.0.0", "typescript": "^5.1.6", "webpack": "^5.21.2", From 70e9183c6baba5ee7118319e7fd37ecb1b9ae2ea Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 24 May 2025 07:19:56 +0200 Subject: [PATCH 598/662] fix(deps): update dependency @google-cloud/language to v7 (#1947) Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 78762231..2a7580fc 100644 --- a/samples/package.json +++ b/samples/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@google-cloud/language": "^6.5.0", + "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^27.0.0", "google-auth-library": "^9.15.1", From 85048302db3e8badb9f941105c0de50101a3fbcc Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Sat, 24 May 2025 18:11:05 -0700 Subject: [PATCH 599/662] Revert "chore(deps): update dependency sinon to v20 (#1953)" (#2018) This reverts commit 79d515907d3b9a62a4b2b5fa8469fcbea4059034. --- .github/scripts/package.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 9d9fc372..baad0f8f 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -16,6 +16,6 @@ "devDependencies": { "@octokit/rest": "^21.0.0", "mocha": "^10.0.0", - "sinon": "^20.0.0" + "sinon": "^18.0.0" } } \ No newline at end of file diff --git a/package.json b/package.json index 783a43d7..2eccce7f 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "nock": "^14.0.1", "null-loader": "^4.0.0", "puppeteer": "^24.0.0", - "sinon": "^20.0.0", + "sinon": "^18.0.1", "ts-loader": "^8.0.0", "typescript": "^5.1.6", "webpack": "^5.21.2", From ed6296b02a18920399f9d2401cfe615d3698ef38 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 30 May 2025 03:25:56 +0200 Subject: [PATCH 600/662] chore(deps): update dependency jsdoc-fresh to v4 (#2021) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2eccce7f..c7ba8b85 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "gts": "^6.0.0", "is-docker": "^3.0.0", "jsdoc": "^4.0.0", - "jsdoc-fresh": "^3.0.0", + "jsdoc-fresh": "^4.0.0", "jsdoc-region-tag": "^3.0.0", "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", From 470bc818700756ebf1f5dcb73b42f331738c0cc6 Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:43:27 -0700 Subject: [PATCH 601/662] style: remove eslint ignores (#1963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: remove all eslint ignores * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .github/scripts/package.json | 2 +- .github/workflows/issues-no-repro.yaml | 2 +- package.json | 2 +- test/test.awsclient.ts | 4 +- test/test.awsrequestsigner.ts | 1 - ...est.externalaccountauthorizeduserclient.ts | 3 +- test/test.externalclient.ts | 8 +-- test/test.googleauth.ts | 55 ++++++++++--------- test/test.identitypoolclient.ts | 29 ++++++---- test/test.index.ts | 1 - test/test.jwt.ts | 26 ++++----- test/test.jwtaccess.ts | 7 +-- test/test.oauth2.ts | 12 ++-- test/test.oauth2common.ts | 16 ++++-- test/test.refresh.ts | 6 +- test/test.stscredentials.ts | 3 +- 16 files changed, 89 insertions(+), 88 deletions(-) diff --git a/.github/scripts/package.json b/.github/scripts/package.json index baad0f8f..2c2e5207 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -14,7 +14,7 @@ "js-yaml": "^4.1.0" }, "devDependencies": { - "@octokit/rest": "^21.0.0", + "@octokit/rest": "^19.0.0", "mocha": "^10.0.0", "sinon": "^18.0.0" } diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 816d9a70..9b2f7014 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,7 +11,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v3 with: node-version: 18 - run: npm install diff --git a/package.json b/package.json index c7ba8b85..3e11ccb9 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "test": "c8 mocha build/test", "clean": "gts clean", "prepare": "npm run compile", - "lint": "gts check", + "lint": "gts check --no-inline-config", "compile": "tsc -p .", "fix": "gts fix", "pretest": "npm run compile -- --sourceMap", diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index c36659fa..c9c5be41 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -36,7 +36,6 @@ const ONE_HOUR_IN_SECS = 3600; describe('AwsClient', () => { let clock: sinon.SinonFakeTimers; - // eslint-disable-next-line @typescript-eslint/no-var-requires const awsSecurityCredentials = require('../../test/fixtures/aws-security-credentials-fake.json'); const referenceDate = new Date('2020-08-11T06:55:22.345Z'); const amzDate = '20200811T065522Z'; @@ -201,8 +200,7 @@ describe('AwsClient', () => { 'No valid AWS "credential_source" provided', ); const invalidCredentialSource = Object.assign({}, awsCredentialSource); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (invalidCredentialSource as any)[required]; + delete (invalidCredentialSource as ReturnType)[required]; const invalidOptions = { type: 'external_account', audience, diff --git a/test/test.awsrequestsigner.ts b/test/test.awsrequestsigner.ts index 197ffef8..7e3e4737 100644 --- a/test/test.awsrequestsigner.ts +++ b/test/test.awsrequestsigner.ts @@ -35,7 +35,6 @@ interface AwsRequestSignerTest { describe('AwsRequestSigner', () => { let clock: sinon.SinonFakeTimers; // Load AWS credentials from a sample security_credentials response. - // eslint-disable-next-line @typescript-eslint/no-var-requires const awsSecurityCredentials = require('../../test/fixtures/aws-security-credentials-fake.json'); const accessKeyId = awsSecurityCredentials.AccessKeyId; const secretAccessKey = awsSecurityCredentials.SecretAccessKey; diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 89f23c21..054d4307 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -49,8 +49,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { interface NockMockRefreshResponse { statusCode: number; response: TokenRefreshResponse | OAuthErrorResponse; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - request: {[key: string]: any}; + request: ReturnType; times?: number; } diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index e573a1c6..d85a420b 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -229,8 +229,9 @@ describe('ExternalAccountClient', () => { it('should return null when given non-ExternalAccountClientOptions', () => { assert( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ExternalAccountClient.fromJSON(serviceAccountKeys as any) === null, + ExternalAccountClient.fromJSON( + serviceAccountKeys as ReturnType, + ) === null, ); }); @@ -246,8 +247,7 @@ describe('ExternalAccountClient', () => { it('should throw when given invalid IdentityPoolClient', () => { const invalidOptions = Object.assign({}, fileSourcedOptions); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (invalidOptions as any).credential_source = {}; + (invalidOptions as ReturnType).credential_source = {}; assert.throws(() => { return ExternalAccountClient.fromJSON(invalidOptions); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 60a0877c..d280226d 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -84,15 +84,10 @@ describe('googleauth', () => { STUB_PROJECT, ].join('/'); - // eslint-disable-next-line @typescript-eslint/no-var-requires const privateJSON = require('../../test/fixtures/private.json'); - // eslint-disable-next-line @typescript-eslint/no-var-requires const private2JSON = require('../../test/fixtures/private2.json'); - // eslint-disable-next-line @typescript-eslint/no-var-requires const refreshJSON = require('../../test/fixtures/refresh.json'); - // eslint-disable-next-line @typescript-eslint/no-var-requires const externalAccountJSON = require('../../test/fixtures/external-account-cred.json'); - // eslint-disable-next-line @typescript-eslint/no-var-requires const externalAccountAuthorizedUserJSON = require('../../test/fixtures/external-account-authorized-user-cred.json'); const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); const wellKnownPathWindows = path.join( @@ -268,8 +263,9 @@ describe('googleauth', () => { function mockGCE() { const scope1 = nockIsGCE(); const auth = new GoogleAuth(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); + sinon + .stub(auth as ReturnType, 'getDefaultServiceProjectId') + .resolves(); const scope2 = nock(HOST_ADDRESS) .get(tokenPath) .reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS); @@ -346,8 +342,7 @@ describe('googleauth', () => { const auth = new GoogleAuth(); assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (auth as any).fromJSON(null); + (auth as ReturnType).fromJSON(null); }); }); @@ -508,8 +503,7 @@ describe('googleauth', () => { it('fromStream should error on null stream', done => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (auth as any).fromStream(null, (err: Error) => { + (auth as ReturnType).fromStream(null, (err: Error) => { assert.strictEqual(true, err instanceof Error); done(); }); @@ -599,8 +593,9 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should error on null file path', async () => { try { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (auth as any)._getApplicationCredentialsFromFilePath(null); + await ( + auth as ReturnType + )._getApplicationCredentialsFromFilePath(null); } catch (e) { return; } @@ -619,8 +614,9 @@ describe('googleauth', () => { it('getApplicationCredentialsFromFilePath should error on non-string file path', async () => { try { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await auth._getApplicationCredentialsFromFilePath(2 as any); + await auth._getApplicationCredentialsFromFilePath( + 2 as ReturnType, + ); } catch (e) { return; } @@ -842,8 +838,7 @@ describe('googleauth', () => { assert.strictEqual(projectId, STUB_PROJECT); // Null out all the private functions that make this method work - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const anyd = auth as any; + const anyd = auth as ReturnType; anyd.getProductionProjectId = null; anyd.getFileProjectId = null; anyd.getDefaultServiceProjectId = null; @@ -969,13 +964,14 @@ describe('googleauth', () => { // Make sure our special test bit is not set yet, indicating that // this is a new credentials instance. // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.strictEqual(undefined, (cachedCredential as any).specialTestBit); + assert.strictEqual( + undefined, + (cachedCredential as ReturnType).specialTestBit, + ); // Now set the special test bit. // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (cachedCredential as any).specialTestBit = 'monkey'; + (cachedCredential as ReturnType).specialTestBit = 'monkey'; // Ask for credentials again, from the same auth instance. We expect // a cached instance this time. @@ -987,8 +983,10 @@ describe('googleauth', () => { // the object instance is the same. // Test verifies invalid parameter tests, which requires cast to // any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.strictEqual('monkey', (result2 as any).specialTestBit); + assert.strictEqual( + 'monkey', + (result2 as ReturnType).specialTestBit, + ); assert.strictEqual(cachedCredential, result2); // Now create a second GoogleAuth instance, and ask for @@ -1000,8 +998,10 @@ describe('googleauth', () => { // Make sure we get a new (non-cached) credential instance back. // Test verifies invalid parameter tests, which requires cast to // any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.strictEqual(undefined, (result3 as any).specialTestBit); + assert.strictEqual( + undefined, + (result3 as ReturnType).specialTestBit, + ); assert.notStrictEqual(cachedCredential, result3); }); @@ -1467,8 +1467,9 @@ describe('googleauth', () => { }); it('should throw if getProjectId cannot find a projectId', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); + sinon + .stub(auth as ReturnType, 'getDefaultServiceProjectId') + .resolves(); await assert.rejects( auth.getProjectId(), /Unable to detect a Project Id in the current environment/, diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 4b5f2d44..0c9a06ed 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -218,8 +218,9 @@ describe('IdentityPoolClient', () => { }; assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new IdentityPoolClient(invalidOptions as any); + return new IdentityPoolClient( + invalidOptions as ReturnType, + ); }, expectedError); }); @@ -238,10 +239,11 @@ describe('IdentityPoolClient', () => { }, }; - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new IdentityPoolClient(invalidOptions as any); - }, expectedError); + assert.throws( + () => + new IdentityPoolClient(invalidOptions as ReturnType), + expectedError, + ); }); it('should throw on invalid credential_source.format.type', () => { @@ -260,8 +262,9 @@ describe('IdentityPoolClient', () => { }; assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new IdentityPoolClient(invalidOptions as any); + return new IdentityPoolClient( + invalidOptions as ReturnType, + ); }, expectedError); }); @@ -315,8 +318,9 @@ describe('IdentityPoolClient', () => { }; assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new IdentityPoolClient(invalidOptions as any); + return new IdentityPoolClient( + invalidOptions as ReturnType, + ); }, expectedError); }); @@ -334,8 +338,9 @@ describe('IdentityPoolClient', () => { }; assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new IdentityPoolClient(invalidOptions as any); + return new IdentityPoolClient( + invalidOptions as ReturnType, + ); }, expectedError); }); diff --git a/test/test.index.ts b/test/test.index.ts index a22b8bde..f5978e80 100644 --- a/test/test.index.ts +++ b/test/test.index.ts @@ -17,7 +17,6 @@ import * as gal from '../src'; describe('index', () => { it('should publicly export GoogleAuth', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const cjs = require('../src/'); assert.strictEqual(cjs.GoogleAuth, gal.GoogleAuth); }); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index df0db7ca..ae005598 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -28,7 +28,6 @@ function removeBearerFromAuthorizationHeader(headers: Headers): string { } describe('jwt', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const keypair = require('keypair'); const PEM_PATH = './test/fixtures/private.pem'; const PEM_CONTENTS = fs.readFileSync(PEM_PATH, 'utf8'); @@ -584,8 +583,7 @@ describe('jwt', () => { it('fromJson should error on null json', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (jwt as any).fromJSON(null); + (jwt as ReturnType).fromJSON(null); }); }); @@ -668,8 +666,7 @@ describe('jwt', () => { it('fromStream should error on null stream', done => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (jwt as any).fromStream(null, (err: Error) => { + (jwt as ReturnType).fromStream(null, (err: Error) => { assert.strictEqual(true, err instanceof Error); done(); }); @@ -703,8 +700,7 @@ describe('jwt', () => { it('fromAPIKey should error without api key', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (jwt as any).fromAPIKey(undefined); + (jwt as ReturnType).fromAPIKey(undefined); }); }); @@ -712,8 +708,7 @@ describe('jwt', () => { const KEY = 'test'; assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - jwt.fromAPIKey({key: KEY} as any); + jwt.fromAPIKey({key: KEY} as ReturnType); }); }); @@ -740,7 +735,6 @@ describe('jwt', () => { it('getRequestHeaders populates x-goog-user-project for JWT client', async () => { const auth = new GoogleAuth({ credentials: Object.assign( - // eslint-disable-next-line @typescript-eslint/no-var-requires require('../../test/fixtures/service-account-with-quota.json'), { private_key: keypair(512 /* bitsize of private key */).private, @@ -1107,8 +1101,10 @@ describe('jwt', () => { it('returns headers from cache, prior to their expiry time', async () => { const sign = sandbox.stub(jws, 'sign').returns('abc123'); const getExpirationTime = sandbox - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .stub(jwtaccess.JWTAccess as any, 'getExpirationTime') + .stub( + jwtaccess.JWTAccess as ReturnType, + 'getExpirationTime', + ) .returns(Date.now() / 1000 + 3600); // expire in an hour. const jwt = new JWT({ email: 'foo@serviceaccount.com', @@ -1128,8 +1124,10 @@ describe('jwt', () => { it('creates a new self-signed JWT, if headers are close to expiring', async () => { const sign = sandbox.stub(jws, 'sign').returns('abc123'); const getExpirationTime = sandbox - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .stub(jwtaccess.JWTAccess as any, 'getExpirationTime') + .stub( + jwtaccess.JWTAccess as ReturnType, + 'getExpirationTime', + ) .returns(Date.now() / 1000 + 5); // expire in 5 seconds. const jwt = new JWT({ email: 'foo@serviceaccount.com', diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index 22d755cd..5aa69ac7 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -20,7 +20,6 @@ import * as sinon from 'sinon'; import {JWTAccess} from '../src'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const keypair = require('keypair'); describe('jwtaccess', () => { @@ -124,8 +123,7 @@ describe('jwtaccess', () => { it('fromJson should error on null json', () => { assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (client as any).fromJSON(null); + (client as ReturnType).fromJSON(null); }); }); @@ -168,8 +166,7 @@ describe('jwtaccess', () => { it('fromStream should error on null stream', done => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (client as any).fromStream(null, (err: Error) => { + (client as ReturnType).fromStream(null, (err: Error) => { assert.strictEqual(true, err instanceof Error); done(); }); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index b8d2818a..a379be67 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -202,8 +202,11 @@ describe('oauth2', () => { return new LoginTicket('c', payload); }; assert.throws( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - () => (client as any).verifyIdToken(idToken, audience), + () => + (client as ReturnType).verifyIdToken( + idToken, + audience, + ), /This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry./, ); }); @@ -1015,8 +1018,9 @@ describe('oauth2', () => { client.credentials = {refresh_token: 'refresh-token-placeholder'}; try { await client.request({url: 'http://example.com'}); - // eslint-disable-next-line no-empty - } catch (e) {} + } catch (e) { + // ignore + } await client.request({url: 'http://example.com'}); scopes.forEach(s => s.done()); assert.strictEqual('abc123', client.credentials.access_token); diff --git a/test/test.oauth2common.ts b/test/test.oauth2common.ts index 1ff92abe..c722b9c9 100644 --- a/test/test.oauth2common.ts +++ b/test/test.oauth2common.ts @@ -36,8 +36,11 @@ class TestOAuthClientAuthHandler extends OAuthClientAuthHandler { /** Custom error object for testing additional fields on an Error. */ class CustomError extends Error { public readonly code?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(message: string, stack?: any, code?: string) { + constructor( + message: string, + stack?: ReturnType, + code?: string, + ) { super(message); this.name = 'CustomError'; this.stack = stack; @@ -164,8 +167,7 @@ describe('OAuthClientAuthHandler', () => { }); describe('with request-body client auth', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const unsupportedMethods: any[] = [ + const unsupportedMethods: ReturnType[] = [ undefined, 'GET', 'DELETE', @@ -491,8 +493,10 @@ describe('getErrorFromOAuthErrorResponse', () => { const actualError = getErrorFromOAuthErrorResponse(resp, originalError); assert.strictEqual(actualError.message, expectedError.message); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.strictEqual((actualError as any).code, expectedError.code); + assert.strictEqual( + (actualError as ReturnType).code, + expectedError.code, + ); assert.strictEqual(actualError.name, expectedError.name); assert.strictEqual(actualError.stack, expectedError.stack); }); diff --git a/test/test.refresh.ts b/test/test.refresh.ts index 79f3f4f2..2902540f 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -40,8 +40,7 @@ describe('refresh', () => { const refresh = new UserRefreshClient(); assert.throws(() => { // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (refresh as any).fromJSON(null); + (refresh as ReturnType).fromJSON(null); }); }); @@ -104,8 +103,7 @@ describe('refresh', () => { it('fromStream should error on null stream', done => { const refresh = new UserRefreshClient(); // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (refresh as any).fromStream(null, (err: Error) => { + (refresh as ReturnType).fromStream(null, (err: Error) => { assert.strictEqual(true, err instanceof Error); done(); }); diff --git a/test/test.stscredentials.ts b/test/test.stscredentials.ts index c78ea056..ebe98174 100644 --- a/test/test.stscredentials.ts +++ b/test/test.stscredentials.ts @@ -89,8 +89,7 @@ describe('StsCredentials', () => { function mockStsTokenExchange( statusCode = 200, response: StsSuccessfulResponse | OAuthErrorResponse, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - request: {[key: string]: any}, + request: ReturnType, additionalHeaders?: {[key: string]: string}, ): nock.Scope { const headers = Object.assign( From 940084c8e6f6213f492443fe7b6d447d890efe4d Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 00:13:11 -0700 Subject: [PATCH 602/662] chore: update NodeJS post-processor to use MOSS approved images and update node version for tests (#2024) * chore: update post-processor to use MOSS approved images * update dockerfile * remove all hyphens * Update Dockerfile * install python from apt-get * Update container_test.yaml * Update container_test.yaml * install node from apt-get * Update container_test.yaml * Update Dockerfile Source-Link: https://github.com/googleapis/synthtool/commit/1752b9049fd76751e49179628ba4f3a0e0844953 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:da69f1fd77b825b0520b1b0a047c270a3f7e3a42e4d46a5321376281cef6e62b Co-authored-by: Owl Bot Co-authored-by: d-goog <188102366+d-goog@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 60443342..f434738c 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:c7e4968cfc97a204a4b2381f3ecb55cabc40c4cccf88b1ef8bef0d976be87fee -# created: 2025-04-08T17:33:08.498793944Z + digest: sha256:da69f1fd77b825b0520b1b0a047c270a3f7e3a42e4d46a5321376281cef6e62b +# created: 2025-06-02T21:06:54.667555755Z From ebb2bc01597688e19cfb95ab61deea1568b0761b Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 5 Jun 2025 19:19:21 +0200 Subject: [PATCH 603/662] fix(deps): update dependency @googleapis/iam to v28 (#2027) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 2a7580fc..1ecb77d5 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^27.0.0", + "@googleapis/iam": "^28.0.0", "google-auth-library": "^9.15.1", "node-fetch": "^2.3.0", "open": "^9.0.0", From c0181c509c8c14c8ed2489c8c839e19ca3f63704 Mon Sep 17 00:00:00 2001 From: miguel Date: Tue, 10 Jun 2025 14:00:07 -0700 Subject: [PATCH 604/662] fix: process undefined values before creating URLSearchParams (#2029) --- src/auth/oauth2client.ts | 5 +++-- src/auth/stscredentials.ts | 12 +++------- src/util.ts | 12 ++++++++++ test/test.oauth2.ts | 46 ++++++++++++++++++++++++++++++++++++++ test/test.util.ts | 23 ++++++++++++++++++- 5 files changed, 86 insertions(+), 12 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 2c70a8ce..99e0398d 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -23,6 +23,7 @@ import * as querystring from 'querystring'; import * as stream from 'stream'; import * as formatEcdsa from 'ecdsa-sig-formatter'; +import {removeUndefinedValuesInObject} from '../util'; import {createCrypto, JwkCertificate, hasBrowserCrypto} from '../crypto/crypto'; import { @@ -754,7 +755,7 @@ export class OAuth2Client extends AuthClient { ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, - data: new URLSearchParams(values as {}), + data: new URLSearchParams(removeUndefinedValuesInObject(values) as {}), headers, }; AuthClient.setMethodName(opts, 'getTokenAsync'); @@ -820,7 +821,7 @@ export class OAuth2Client extends AuthClient { ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, - data: new URLSearchParams(data), + data: new URLSearchParams(removeUndefinedValuesInObject(data)), }; AuthClient.setMethodName(opts, 'refreshTokenNoCache'); diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 65175f1f..0beaf3ca 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -22,6 +22,8 @@ import { getErrorFromOAuthErrorResponse, } from './oauth2common'; +import {removeUndefinedValuesInObject} from '../util'; + /** * Defines the interface needed to initialize an StsCredentials instance. * The interface does not directly map to the spec and instead is converted @@ -203,20 +205,12 @@ export class StsCredentials extends OAuthClientAuthHandler { options: options && JSON.stringify(options), }; - // Keep defined fields. - const payload: Record = {}; - Object.entries(values).forEach(([key, value]) => { - if (value !== undefined) { - payload[key] = value; - } - }); - const opts: GaxiosOptions = { ...StsCredentials.RETRY_CONFIG, url: this.#tokenExchangeEndpoint.toString(), method: 'POST', headers, - data: new URLSearchParams(payload), + data: new URLSearchParams(removeUndefinedValuesInObject(values)), }; AuthClient.setMethodName(opts, 'exchangeToken'); diff --git a/src/util.ts b/src/util.ts index 945137c5..99a01184 100644 --- a/src/util.ts +++ b/src/util.ts @@ -239,3 +239,15 @@ export class LRUCache { } } } + +// Given and object remove fields where value is undefined. +export function removeUndefinedValuesInObject(object: {[key: string]: any}): { + [key: string]: any; +} { + Object.entries(object).forEach(([key, value]) => { + if (value === undefined || value === 'undefined') { + delete object[key]; + } + }); + return object; +} diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index a379be67..fe4f656c 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -1370,6 +1370,52 @@ describe('oauth2', () => { assert.strictEqual(params.get('code_verifier'), 'its_verified'); }); + it('getToken should ignore undefined code verifier', async () => { + const scope = nock(baseUrl, { + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, + }) + .post('/token') + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + const res = await client.getToken({ + code: 'code here', + codeVerifier: undefined, + }); + scope.done(); + assert(res.res); + + const params = new URLSearchParams(res.res.config.data || ''); + assert.strictEqual(params.get('code_verifier'), null); + }); + + it('getToken should ignore undefined string code verifier', async () => { + const scope = nock(baseUrl, { + reqheaders: { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, + }) + .post('/token') + .reply(200, { + access_token: 'abc', + refresh_token: '123', + expires_in: 10, + }); + const res = await client.getToken({ + code: 'code here', + codeVerifier: 'undefined', + }); + scope.done(); + assert(res.res); + + const params = new URLSearchParams(res.res.config.data || ''); + assert.strictEqual(params.get('code_verifier'), null); + }); + it('getToken should set redirect_uri if not provided in options', async () => { const scope = nock(baseUrl, { reqheaders: { diff --git a/test/test.util.ts b/test/test.util.ts index 6f584c9c..d167ba5b 100644 --- a/test/test.util.ts +++ b/test/test.util.ts @@ -15,7 +15,7 @@ import {strict as assert} from 'assert'; import * as sinon from 'sinon'; -import {LRUCache} from '../src/util'; +import {LRUCache, removeUndefinedValuesInObject} from '../src/util'; describe('util', () => { let sandbox: sinon.SinonSandbox; @@ -81,3 +81,24 @@ describe('util', () => { }); }); }); + +describe('util removeUndefinedValuesInObject', () => { + it('remove undefined type values in object', () => { + const object: {[key: string]: any} = { + undefined: undefined, + number: 1, + }; + assert.deepEqual(removeUndefinedValuesInObject(object), { + number: 1, + }); + }); + it('remove undefined string values in object', () => { + const object: {[key: string]: any} = { + undefined: 'undefined', + number: 1, + }; + assert.deepEqual(removeUndefinedValuesInObject(object), { + number: 1, + }); + }); +}); From 8c8cb22d9ba1579afbf207af1871e1819d1b5c26 Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:18:55 -0700 Subject: [PATCH 605/662] chore: update gcp-metadata, gtoken, and gaxios to major versions (#2030) --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 3e11ccb9..8b328b52 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,10 @@ "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0-rc.4", - "gcp-metadata": "^7.0.0-rc.1", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0-rc.1", + "gtoken": "^8.0.0", "jws": "^4.0.0" }, "devDependencies": { From 5d3f4913fffde808efa98716a3cec06ff0dc481c Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:54:52 -0700 Subject: [PATCH 606/662] chore: skip storage sample tests since storage hasn't migrated to 10 *will need to revert* (#2032) * chore: update auth.test.js * chore: update downscoping-with-cab.test.js --- samples/test/auth.test.js | 3 ++- samples/test/downscoping-with-cab.test.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/samples/test/auth.test.js b/samples/test/auth.test.js index 249e52f5..723f29ae 100644 --- a/samples/test/auth.test.js +++ b/samples/test/auth.test.js @@ -30,7 +30,8 @@ const execSync = (command, opts) => { }; describe('auth samples', () => { - it('should authenticate explicitly', async () => { + // TODO: un-skip once storage is migrated: https://github.com/googleapis/nodejs-storage/pull/2592 + it.skip('should authenticate explicitly', async () => { const output = execSync('node authenticateExplicit'); assert.match(output, /Listed all storage buckets./); diff --git a/samples/test/downscoping-with-cab.test.js b/samples/test/downscoping-with-cab.test.js index 55f0912c..31d895cb 100644 --- a/samples/test/downscoping-with-cab.test.js +++ b/samples/test/downscoping-with-cab.test.js @@ -49,7 +49,8 @@ const execAsync = async (cmd, opts) => { }; describe('samples for downscoping with cab', () => { - it('should have access to the object specified in the cab rule', async () => { + // TODO: un-skip once storage is migrated: https://github.com/googleapis/nodejs-storage/pull/2592 + it.skip('should have access to the object specified in the cab rule', async () => { const output = await execAsync(`${process.execPath} downscopedclient`, { env: { ...process.env, From e0438765a25d03d6ab72c870838ae870d43650bc Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:02:12 -0700 Subject: [PATCH 607/662] chore(main): release 10.0.0 (#1927) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc942a7d..e798ba01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,48 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.0.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.15.1...v10.0.0) (2025-06-11) + + +### ⚠ BREAKING CHANGES + +* `Request` Revamp ([#1938](https://github.com/googleapis/google-auth-library-nodejs/issues/1938)) +* Remove `Transporter` ([#1937](https://github.com/googleapis/google-auth-library-nodejs/issues/1937)) +* remove additionalOptions from AuthClients ([#1689](https://github.com/googleapis/google-auth-library-nodejs/issues/1689)) +* Remove `options.ts` ([#1920](https://github.com/googleapis/google-auth-library-nodejs/issues/1920)) +* Remove `messages.ts` ([#1919](https://github.com/googleapis/google-auth-library-nodejs/issues/1919)) +* Support Node 18+ ([#1879](https://github.com/googleapis/google-auth-library-nodejs/issues/1879)) +* Support Node 18, 20, and 22 ([#1928](https://github.com/googleapis/google-auth-library-nodejs/issues/1928)) +* remove DEFAULT_UNIVERSE from BaseExternalClient ([#1690](https://github.com/googleapis/google-auth-library-nodejs/issues/1690)) +* Move Base AuthClient Types to authclient.ts ([#1774](https://github.com/googleapis/google-auth-library-nodejs/issues/1774)) + +### Features + +* `Request` Revamp ([#1938](https://github.com/googleapis/google-auth-library-nodejs/issues/1938)) ([f23e807](https://github.com/googleapis/google-auth-library-nodejs/commit/f23e807e27a1d64f774f2bf25e01d263f1ce7db1)) +* Add debug logging support ([#1903](https://github.com/googleapis/google-auth-library-nodejs/issues/1903)) ([13ca1dc](https://github.com/googleapis/google-auth-library-nodejs/commit/13ca1dcad1f79e2015fa4326287caf9b43bc4cf2)) +* Support Node 18, 20, and 22 ([#1928](https://github.com/googleapis/google-auth-library-nodejs/issues/1928)) ([5b60a95](https://github.com/googleapis/google-auth-library-nodejs/commit/5b60a95d1759fcd4a3a3614e8345203a4e1d29f2)) +* Support Node 18+ ([#1879](https://github.com/googleapis/google-auth-library-nodejs/issues/1879)) ([3d24045](https://github.com/googleapis/google-auth-library-nodejs/commit/3d240453d48a092db4b43c6f000751160ef1dea4)) + + +### Bug Fixes + +* Circular Dependencies Issue ([#1936](https://github.com/googleapis/google-auth-library-nodejs/issues/1936)) ([aea893c](https://github.com/googleapis/google-auth-library-nodejs/commit/aea893cd03bebee884692bc1bf21c483f89345f7)) +* **deps:** Update dependency @google-cloud/language to v7 ([#1947](https://github.com/googleapis/google-auth-library-nodejs/issues/1947)) ([70e9183](https://github.com/googleapis/google-auth-library-nodejs/commit/70e9183c6baba5ee7118319e7fd37ecb1b9ae2ea)) +* **deps:** Update dependency @googleapis/iam to v27 ([#1962](https://github.com/googleapis/google-auth-library-nodejs/issues/1962)) ([629da74](https://github.com/googleapis/google-auth-library-nodejs/commit/629da74095ae51c258de13d924b65bb3c31ddf82)) +* **deps:** Update dependency @googleapis/iam to v28 ([#2027](https://github.com/googleapis/google-auth-library-nodejs/issues/2027)) ([ebb2bc0](https://github.com/googleapis/google-auth-library-nodejs/commit/ebb2bc01597688e19cfb95ab61deea1568b0761b)) +* **deps:** Update dependency puppeteer to v24 ([#1933](https://github.com/googleapis/google-auth-library-nodejs/issues/1933)) ([474453d](https://github.com/googleapis/google-auth-library-nodejs/commit/474453d64a3ba2a1cfba6d1527a2cd6e60bda62d)) +* Process undefined values before creating URLSearchParams ([#2029](https://github.com/googleapis/google-auth-library-nodejs/issues/2029)) ([c0181c5](https://github.com/googleapis/google-auth-library-nodejs/commit/c0181c509c8c14c8ed2489c8c839e19ca3f63704)) + + +### Code Refactoring + +* Move Base AuthClient Types to authclient.ts ([#1774](https://github.com/googleapis/google-auth-library-nodejs/issues/1774)) ([b0c3a43](https://github.com/googleapis/google-auth-library-nodejs/commit/b0c3a43124860530a567a3529f8ac41b6c7d20c5)) +* Remove `messages.ts` ([#1919](https://github.com/googleapis/google-auth-library-nodejs/issues/1919)) ([654753d](https://github.com/googleapis/google-auth-library-nodejs/commit/654753dc6a85bfefeee4d3c87439183deb13212d)) +* Remove `options.ts` ([#1920](https://github.com/googleapis/google-auth-library-nodejs/issues/1920)) ([51316e8](https://github.com/googleapis/google-auth-library-nodejs/commit/51316e8e75f111b897b284cc77d8429e4db8e25a)) +* Remove `Transporter` ([#1937](https://github.com/googleapis/google-auth-library-nodejs/issues/1937)) ([dbcc44b](https://github.com/googleapis/google-auth-library-nodejs/commit/dbcc44bf73c494361f331b3423c679cc2d19d51f)) +* Remove additionalOptions from AuthClients ([#1689](https://github.com/googleapis/google-auth-library-nodejs/issues/1689)) ([2f780a8](https://github.com/googleapis/google-auth-library-nodejs/commit/2f780a85e11fe2cfb0dbf7f91dfbd90d15207491)) +* Remove DEFAULT_UNIVERSE from BaseExternalClient ([#1690](https://github.com/googleapis/google-auth-library-nodejs/issues/1690)) ([4f1dc04](https://github.com/googleapis/google-auth-library-nodejs/commit/4f1dc0476ccbfba26043aa2dab6673bc03a0787d)) + ## [9.15.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.15.0...v9.15.1) (2025-01-24) diff --git a/package.json b/package.json index 8b328b52..34b3dc68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "9.15.1", + "version": "10.0.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 1ecb77d5..229885fb 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^28.0.0", - "google-auth-library": "^9.15.1", + "google-auth-library": "^10.0.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From 182c5dba3ef9bcf9ae7eb2d3c7167d4fe899c118 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 11 Jun 2025 20:42:15 +0200 Subject: [PATCH 608/662] fix(deps): update dependency google-auth-library to v10 (#2034) --- samples/puppeteer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/puppeteer/package.json b/samples/puppeteer/package.json index 797c1247..fd61d546 100644 --- a/samples/puppeteer/package.json +++ b/samples/puppeteer/package.json @@ -11,7 +11,7 @@ }, "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^9.0.0", + "google-auth-library": "^10.0.0", "puppeteer": "^24.0.0" } } From c63f608ce33a3ea95b70f39eadcf0a4415e5657c Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:59:49 -0700 Subject: [PATCH 609/662] feat: `fetch`-Compatible API (#1939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: `fetch`-Compatible API * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: fix copyright * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: missing type from merge * style: lint * docs: update README * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- .readme-partials.yaml | 152 ++++++++++++++------------------- README.md | 152 ++++++++++++++------------------- samples/adc.js | 4 +- samples/compute.js | 4 +- samples/credentials.js | 4 +- samples/idtokens-iap.js | 2 +- samples/idtokens-serverless.js | 2 +- samples/jwt.js | 2 +- samples/keyfile.js | 2 +- src/auth/authclient.ts | 71 ++++++++++++++- src/auth/googleauth.ts | 28 ++++++ test/test.authclient.ts | 54 ++++++++++++ test/test.googleauth.ts | 37 +++++++- 13 files changed, 323 insertions(+), 191 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 480509b6..514b849a 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -40,18 +40,14 @@ body: |- * Instead of specifying the type of client you'd like to use (JWT, OAuth2, etc) * this library will automatically choose the right client based on the environment. */ - async function main() { - const auth = new GoogleAuth({ - scopes: 'https://www.googleapis.com/auth/cloud-platform' - }); - const client = await auth.getClient(); - const projectId = await auth.getProjectId(); - const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({ url }); - console.log(res.data); - } - - main().catch(console.error); + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform' + }); + const projectId = await auth.getProjectId(); + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; + // The modern `fetch` and classic `request` APIs are available + const res = await auth.fetch(url); + console.log(res.data); ``` ## OAuth2 @@ -81,10 +77,11 @@ body: |- */ async function main() { const oAuth2Client = await getAuthenticatedClient(); - // Make a simple request to the People API using our pre-authenticated client. The `request()` method - // takes an GaxiosOptions object. Visit https://github.com/JustinBeckwith/gaxios. + // Make a simple request to the People API using our pre-authenticated client. The `fetch` and + // `request` methods accept a [`GaxiosOptions`](https://github.com/googleapis/gaxios) + // object. const url = 'https://people.googleapis.com/v1/people/me?personFields=names'; - const res = await oAuth2Client.request({url}); + const res = await oAuth2Client.fetch(url); console.log(res.data); // After acquiring an access_token, you may want to check on the audience, expiration, @@ -156,6 +153,7 @@ body: |- This library will automatically obtain an `access_token`, and automatically refresh the `access_token` if a `refresh_token` is present. The `refresh_token` is only returned on the [first authorization](https://github.com/googleapis/google-api-nodejs-client/issues/750#issuecomment-304521450), so if you want to make sure you store it safely. An easy way to make sure you always store the most recent tokens is to use the `tokens` event: ```js + const auth = new GoogleAuth(); const client = await auth.getClient(); client.on('tokens', (tokens) => { @@ -166,9 +164,10 @@ body: |- console.log(tokens.access_token); }); + const projectId = await auth.getProjectId(); const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({ url }); // The `tokens` event would now be raised if this was the first request + const res = await client.fetch(url); ``` #### Retrieve access token @@ -241,18 +240,14 @@ body: |- const {JWT} = require('google-auth-library'); const keys = require('./jwt.keys.json'); - async function main() { - const client = new JWT({ - email: keys.client_email, - key: keys.private_key, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }); - const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; - const res = await client.request({url}); - console.log(res.data); - } - - main().catch(console.error); + const client = new JWT({ + email: keys.client_email, + key: keys.private_key, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; + const res = await client.fetch(url); + console.log(res.data); ``` The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js). @@ -288,16 +283,12 @@ body: |- } const keys = JSON.parse(keysEnvVar); - async function main() { - // load the JWT or UserRefreshClient from the keys - const client = auth.fromJSON(keys); - client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; - const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; - const res = await client.request({url}); - console.log(res.data); - } - - main().catch(console.error); + // load the JWT or UserRefreshClient from the keys + const client = auth.fromJSON(keys); + client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; + const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; + const res = await client.fetch(url); + console.log(res.data); ``` **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). @@ -313,18 +304,14 @@ body: |- ``` js const {auth, Compute} = require('google-auth-library'); - async function main() { - const client = new Compute({ - // Specifying the service account email is optional. - serviceAccountEmail: 'my-service-account@example.com' - }); - const projectId = await auth.getProjectId(); - const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({url}); - console.log(res.data); - } - - main().catch(console.error); + const client = new Compute({ + // Specifying the service account email is optional. + serviceAccountEmail: 'my-service-account@example.com' + }); + const projectId = await auth.getProjectId(); + const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; + const res = await client.fetch(url); + console.log(res.data); ``` ## Workload Identity Federation @@ -1023,17 +1010,14 @@ body: |- The library can now automatically choose the right type of client and initialize credentials from the context provided in the configuration file. ```js - async function main() { - const auth = new GoogleAuth({ - scopes: 'https://www.googleapis.com/auth/cloud-platform' - }); - const client = await auth.getClient(); - const projectId = await auth.getProjectId(); - // List all buckets in a project. - const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; - const res = await client.request({ url }); - console.log(res.data); - } + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform' + }); + const projectId = await auth.getProjectId(); + // List all buckets in a project. + const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; + const res = await client.fetch(url); + console.log(res.data); ``` When using external identities with Application Default Credentials in Node.js, the `roles/browser` role needs to be granted to the service account. @@ -1056,14 +1040,12 @@ body: |- const {ExternalAccountClient} = require('google-auth-library'); const jsonConfig = require('/path/to/config.json'); - async function main() { - const client = ExternalAccountClient.fromJSON(jsonConfig); - client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; - // List all buckets in a project. - const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; - const res = await client.request({url}); - console.log(res.data); - } + const client = ExternalAccountClient.fromJSON(jsonConfig); + client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; + // List all buckets in a project. + const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; + const res = await client.fetch(url); + console.log(res.data); ``` #### Security Considerations @@ -1087,15 +1069,11 @@ body: |- // Make a request to a protected Cloud Run service. const {GoogleAuth} = require('google-auth-library'); - async function main() { - const url = 'https://cloud-run-1234-uc.a.run.app'; - const auth = new GoogleAuth(); - const client = await auth.getIdTokenClient(url); - const res = await client.request({url}); - console.log(res.data); - } - - main().catch(console.error); + const url = 'https://cloud-run-1234-uc.a.run.app'; + const auth = new GoogleAuth(); + const client = await auth.getIdTokenClient(url); + const res = await client.fetch(url); + console.log(res.data); ``` A complete example can be found in [`samples/idtokens-serverless.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js). @@ -1107,16 +1085,12 @@ body: |- // Make a request to a protected Cloud Identity-Aware Proxy (IAP) resource const {GoogleAuth} = require('google-auth-library'); - async function main() - const targetAudience = 'iap-client-id'; - const url = 'https://iap-url.com'; - const auth = new GoogleAuth(); - const client = await auth.getIdTokenClient(targetAudience); - const res = await client.request({url}); - console.log(res.data); - } - - main().catch(console.error); + const targetAudience = 'iap-client-id'; + const url = 'https://iap-url.com'; + const auth = new GoogleAuth(); + const client = await auth.getIdTokenClient(targetAudience); + const res = await client.fetch(url); + console.log(res.data); ``` A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js). @@ -1189,7 +1163,7 @@ body: |- // Use impersonated credentials: const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID' - const resp = await targetClient.request({ url }); + const resp = await targetClient.fetch(url); for (const bucket of resp.data.items) { console.log(bucket.name); } diff --git a/README.md b/README.md index a4594640..17ee40cb 100644 --- a/README.md +++ b/README.md @@ -84,18 +84,14 @@ const {GoogleAuth} = require('google-auth-library'); * Instead of specifying the type of client you'd like to use (JWT, OAuth2, etc) * this library will automatically choose the right client based on the environment. */ -async function main() { - const auth = new GoogleAuth({ - scopes: 'https://www.googleapis.com/auth/cloud-platform' - }); - const client = await auth.getClient(); - const projectId = await auth.getProjectId(); - const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({ url }); - console.log(res.data); -} - -main().catch(console.error); +const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform' +}); +const projectId = await auth.getProjectId(); +const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; +// The modern `fetch` and classic `request` APIs are available +const res = await auth.fetch(url); +console.log(res.data); ``` ## OAuth2 @@ -125,10 +121,11 @@ const keys = require('./oauth2.keys.json'); */ async function main() { const oAuth2Client = await getAuthenticatedClient(); - // Make a simple request to the People API using our pre-authenticated client. The `request()` method - // takes an GaxiosOptions object. Visit https://github.com/JustinBeckwith/gaxios. + // Make a simple request to the People API using our pre-authenticated client. The `fetch` and + // `request` methods accept a [`GaxiosOptions`](https://github.com/googleapis/gaxios) + // object. const url = 'https://people.googleapis.com/v1/people/me?personFields=names'; - const res = await oAuth2Client.request({url}); + const res = await oAuth2Client.fetch(url); console.log(res.data); // After acquiring an access_token, you may want to check on the audience, expiration, @@ -200,6 +197,7 @@ main().catch(console.error); This library will automatically obtain an `access_token`, and automatically refresh the `access_token` if a `refresh_token` is present. The `refresh_token` is only returned on the [first authorization](https://github.com/googleapis/google-api-nodejs-client/issues/750#issuecomment-304521450), so if you want to make sure you store it safely. An easy way to make sure you always store the most recent tokens is to use the `tokens` event: ```js +const auth = new GoogleAuth(); const client = await auth.getClient(); client.on('tokens', (tokens) => { @@ -210,9 +208,10 @@ client.on('tokens', (tokens) => { console.log(tokens.access_token); }); +const projectId = await auth.getProjectId(); const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; -const res = await client.request({ url }); // The `tokens` event would now be raised if this was the first request +const res = await client.fetch(url); ``` #### Retrieve access token @@ -285,18 +284,14 @@ The Google Developers Console provides a `.json` file that you can use to config const {JWT} = require('google-auth-library'); const keys = require('./jwt.keys.json'); -async function main() { - const client = new JWT({ - email: keys.client_email, - key: keys.private_key, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }); - const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; - const res = await client.request({url}); - console.log(res.data); -} - -main().catch(console.error); +const client = new JWT({ + email: keys.client_email, + key: keys.private_key, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], +}); +const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; +const res = await client.fetch(url); +console.log(res.data); ``` The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js). @@ -332,16 +327,12 @@ if (!keysEnvVar) { } const keys = JSON.parse(keysEnvVar); -async function main() { - // load the JWT or UserRefreshClient from the keys - const client = auth.fromJSON(keys); - client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; - const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; - const res = await client.request({url}); - console.log(res.data); -} - -main().catch(console.error); +// load the JWT or UserRefreshClient from the keys +const client = auth.fromJSON(keys); +client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; +const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; +const res = await client.fetch(url); +console.log(res.data); ``` **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). @@ -357,18 +348,14 @@ If your application is running on Google Cloud Platform, you can authenticate us ``` js const {auth, Compute} = require('google-auth-library'); -async function main() { - const client = new Compute({ - // Specifying the service account email is optional. - serviceAccountEmail: 'my-service-account@example.com' - }); - const projectId = await auth.getProjectId(); - const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({url}); - console.log(res.data); -} - -main().catch(console.error); +const client = new Compute({ + // Specifying the service account email is optional. + serviceAccountEmail: 'my-service-account@example.com' +}); +const projectId = await auth.getProjectId(); +const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; +const res = await client.fetch(url); +console.log(res.data); ``` ## Workload Identity Federation @@ -1067,17 +1054,14 @@ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/config.json The library can now automatically choose the right type of client and initialize credentials from the context provided in the configuration file. ```js -async function main() { - const auth = new GoogleAuth({ - scopes: 'https://www.googleapis.com/auth/cloud-platform' - }); - const client = await auth.getClient(); - const projectId = await auth.getProjectId(); - // List all buckets in a project. - const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; - const res = await client.request({ url }); - console.log(res.data); -} +const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform' +}); +const projectId = await auth.getProjectId(); +// List all buckets in a project. +const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; +const res = await client.fetch(url); +console.log(res.data); ``` When using external identities with Application Default Credentials in Node.js, the `roles/browser` role needs to be granted to the service account. @@ -1100,14 +1084,12 @@ You can also explicitly initialize external account clients using the generated const {ExternalAccountClient} = require('google-auth-library'); const jsonConfig = require('/path/to/config.json'); -async function main() { - const client = ExternalAccountClient.fromJSON(jsonConfig); - client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; - // List all buckets in a project. - const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; - const res = await client.request({url}); - console.log(res.data); -} +const client = ExternalAccountClient.fromJSON(jsonConfig); +client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; +// List all buckets in a project. +const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`; +const res = await client.fetch(url); +console.log(res.data); ``` #### Security Considerations @@ -1131,15 +1113,11 @@ IAM permission. // Make a request to a protected Cloud Run service. const {GoogleAuth} = require('google-auth-library'); -async function main() { - const url = 'https://cloud-run-1234-uc.a.run.app'; - const auth = new GoogleAuth(); - const client = await auth.getIdTokenClient(url); - const res = await client.request({url}); - console.log(res.data); -} - -main().catch(console.error); +const url = 'https://cloud-run-1234-uc.a.run.app'; +const auth = new GoogleAuth(); +const client = await auth.getIdTokenClient(url); +const res = await client.fetch(url); +console.log(res.data); ``` A complete example can be found in [`samples/idtokens-serverless.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js). @@ -1151,16 +1129,12 @@ used when you set up your protected resource as the target audience. // Make a request to a protected Cloud Identity-Aware Proxy (IAP) resource const {GoogleAuth} = require('google-auth-library'); -async function main() - const targetAudience = 'iap-client-id'; - const url = 'https://iap-url.com'; - const auth = new GoogleAuth(); - const client = await auth.getIdTokenClient(targetAudience); - const res = await client.request({url}); - console.log(res.data); -} - -main().catch(console.error); +const targetAudience = 'iap-client-id'; +const url = 'https://iap-url.com'; +const auth = new GoogleAuth(); +const client = await auth.getIdTokenClient(targetAudience); +const res = await client.fetch(url); +console.log(res.data); ``` A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js). @@ -1233,7 +1207,7 @@ async function main() { // Use impersonated credentials: const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID' - const resp = await targetClient.request({ url }); + const resp = await targetClient.fetch(url); for (const bucket of resp.data.items) { console.log(bucket.name); } diff --git a/samples/adc.js b/samples/adc.js index 2e0fc01c..60ab4c9c 100644 --- a/samples/adc.js +++ b/samples/adc.js @@ -1,4 +1,4 @@ -// Copyright 2018, Google, LLC. +// Copyright 2018 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 @@ -28,7 +28,7 @@ async function main() { const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({url}); + const res = await client.fetch(url); console.log('DNS Info:'); console.log(res.data); } diff --git a/samples/compute.js b/samples/compute.js index b4f22596..043d6647 100644 --- a/samples/compute.js +++ b/samples/compute.js @@ -1,4 +1,4 @@ -// Copyright 2018, Google, LLC. +// Copyright 2018 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 @@ -28,7 +28,7 @@ async function main() { }); const projectId = await auth.getProjectId(); const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({url}); + const res = await client.fetch(url); console.log(res.data); } diff --git a/samples/credentials.js b/samples/credentials.js index 5791490e..6c8ab8bb 100644 --- a/samples/credentials.js +++ b/samples/credentials.js @@ -1,4 +1,4 @@ -// Copyright 2018, Google, LLC. +// Copyright 2018 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 @@ -43,7 +43,7 @@ async function main() { const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({url}); + const res = await client.fetch(url); console.log('DNS Info:'); console.log(res.data); } diff --git a/samples/idtokens-iap.js b/samples/idtokens-iap.js index 1575ed82..47289844 100644 --- a/samples/idtokens-iap.js +++ b/samples/idtokens-iap.js @@ -35,7 +35,7 @@ function main( async function request() { console.info(`request IAP ${url} with target audience ${targetAudience}`); const client = await auth.getIdTokenClient(targetAudience); - const res = await client.request({url}); + const res = await client.fetch(url); console.info(res.data); } diff --git a/samples/idtokens-serverless.js b/samples/idtokens-serverless.js index e48a9ae1..559cf258 100644 --- a/samples/idtokens-serverless.js +++ b/samples/idtokens-serverless.js @@ -65,7 +65,7 @@ function main( // Alternatively, one can use `client.idTokenProvider.fetchIdToken` // to return the ID Token. - const res = await client.request({url}); + const res = await client.fetch(url); console.info(res.data); } diff --git a/samples/jwt.js b/samples/jwt.js index 8bf91998..3e416d63 100644 --- a/samples/jwt.js +++ b/samples/jwt.js @@ -38,7 +38,7 @@ async function main( scopes: ['https://www.googleapis.com/auth/cloud-platform'], }); const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; - const res = await client.request({url}); + const res = await client.fetch(url); console.log('DNS Info:'); console.log(res.data); diff --git a/samples/keyfile.js b/samples/keyfile.js index 4a977d38..2d77b406 100644 --- a/samples/keyfile.js +++ b/samples/keyfile.js @@ -33,7 +33,7 @@ async function main( const client = await auth.getClient(); const projectId = await auth.getProjectId(); const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`; - const res = await client.request({url}); + const res = await client.fetch(url); console.log('DNS Info:'); console.log(res.data); } diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 5f47200d..f18dd58e 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -21,6 +21,18 @@ import {log as makeLog} from 'google-logging-utils'; import {PRODUCT_NAME, USER_AGENT} from '../shared.cjs'; +/** + * An interface for enforcing `fetch`-type compliance. + * + * @remarks + * + * This provides type guarantees during build-time, ensuring the `fetch` method is 1:1 + * compatible with the `Gaxios#fetch` API. + */ +interface GaxiosFetchCompliance { + fetch: typeof fetch | Gaxios['fetch']; +} + /** * Easy access to symbol-indexed strings on config objects. */ @@ -203,7 +215,7 @@ export declare interface AuthClient { */ export abstract class AuthClient extends EventEmitter - implements CredentialsClient + implements CredentialsClient, GaxiosFetchCompliance { apiKey?: string; projectId?: string | null; @@ -262,9 +274,66 @@ export abstract class AuthClient this.forceRefreshOnFailure = opts.forceRefreshOnFailure ?? false; } + /** + * A {@link fetch `fetch`} compliant API for {@link AuthClient}. + * + * @see {@link AuthClient.request} for the classic method. + * + * @remarks + * + * This is useful as a drop-in replacement for `fetch` API usage. + * + * @example + * + * ```ts + * const authClient = new AuthClient(); + * const fetchWithAuthClient: typeof fetch = (...args) => authClient.fetch(...args); + * await fetchWithAuthClient('https://example.com'); + * ``` + * + * @param args `fetch` API or {@link Gaxios.fetch `Gaxios#fetch`} parameters + * @returns the {@link GaxiosResponse} with Gaxios-added properties + */ + fetch(...args: Parameters): GaxiosPromise { + // Up to 2 parameters in either overload + const input = args[0]; + const init = args[1]; + + let url: URL | undefined = undefined; + const headers = new Headers(); + + // prepare URL + if (typeof input === 'string') { + url = new URL(input); + } else if (input instanceof URL) { + url = input; + } else if (input && input.url) { + url = new URL(input.url); + } + + // prepare headers + if (input && typeof input === 'object' && 'headers' in input) { + Gaxios.mergeHeaders(headers, input.headers); + } + if (init) { + Gaxios.mergeHeaders(headers, new Headers(init.headers)); + } + + // prepare request + if (typeof input === 'object' && !(input instanceof URL)) { + // input must have been a non-URL object + return this.request({...init, ...input, headers, url}); + } else { + // input must have been a string or URL + return this.request({...init, headers, url}); + } + } + /** * The public request API in which credentials may be added to the request. * + * @see {@link AuthClient.fetch} for the modern method. + * * @param options options for `gaxios` */ abstract request(options: GaxiosOptions): GaxiosPromise; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index eadd78bb..b4d6d04f 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -1078,9 +1078,37 @@ export class GoogleAuth { return opts; } + /** + * A {@link fetch `fetch`} compliant API for {@link GoogleAuth}. + * + * @see {@link GoogleAuth.request} for the classic method. + * + * @remarks + * + * This is useful as a drop-in replacement for `fetch` API usage. + * + * @example + * + * ```ts + * const auth = new GoogleAuth(); + * const fetchWithAuth: typeof fetch = (...args) => auth.fetch(...args); + * await fetchWithAuth('https://example.com'); + * ``` + * + * @param args `fetch` API or {@link Gaxios.fetch `Gaxios#fetch`} parameters + * @returns the {@link GaxiosResponse} with Gaxios-added properties + */ + async fetch(...args: Parameters) { + const client = await this.getClient(); + return client.fetch(...args); + } + /** * Automatically obtain application default credentials, and make an * HTTP request using the given options. + * + * @see {@link GoogleAuth.fetch} for the modern method. + * * @param opts Axios request options for the HTTP request. */ async request(opts: GaxiosOptions): Promise> { diff --git a/test/test.authclient.ts b/test/test.authclient.ts index 235bfc9b..e3e47fbf 100644 --- a/test/test.authclient.ts +++ b/test/test.authclient.ts @@ -14,9 +14,11 @@ import {strict as assert} from 'assert'; +import * as nock from 'nock'; import { Gaxios, GaxiosError, + GaxiosOptions, GaxiosOptionsPrepared, GaxiosResponse, } from 'gaxios'; @@ -72,6 +74,58 @@ describe('AuthClient', () => { } }); + describe('fetch', () => { + const url = 'https://google.com'; + + it('should accept a `string`', async () => { + const scope = nock(url).get('/').reply(200, {}); + + const authClient = new PassThroughClient(); + const res = await authClient.fetch(url); + + scope.done(); + assert(typeof url === 'string'); + assert.deepStrictEqual(res.data, {}); + }); + + it('should accept a `URL`', async () => { + const scope = nock(url).get('/').reply(200, {}); + + const authClient = new PassThroughClient(); + const res = await authClient.fetch(new URL(url)); + + scope.done(); + assert.deepStrictEqual(res.data, {}); + }); + + it('should accept an input with initialization', async () => { + const scope = nock(url).post('/', 'abc').reply(200, {}); + + const authClient = new PassThroughClient(); + const res = await authClient.fetch(url, { + body: Buffer.from('abc'), + method: 'POST', + }); + + scope.done(); + assert.deepStrictEqual(res.data, {}); + }); + + it('should accept `GaxiosOptions`', async () => { + const scope = nock(url).post('/', 'abc').reply(200, {}); + + const authClient = new PassThroughClient(); + const options: GaxiosOptions = { + body: Buffer.from('abc'), + method: 'POST', + }; + const res = await authClient.fetch(url, options); + + scope.done(); + assert.deepStrictEqual(res.data, {}); + }); + }); + describe('shared auth interceptors', () => { it('should use the default interceptors', () => { const gaxios = new Gaxios(); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index d280226d..d203ee61 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1401,7 +1401,18 @@ describe('googleauth', () => { assert.strictEqual(env, envDetect.GCPEnv.CLOUD_RUN); }); - it('should make the request', async () => { + it('should make the request via `#fetch`', async () => { + const url = 'http://example.com'; + const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); + const data = {breakfast: 'coffee'}; + scopes.push(nock(url).get('/').reply(200, data)); + const res = await auth.fetch(url); + scopes.forEach(s => s.done()); + assert.deepStrictEqual(res.data, data); + }); + + it('should make the request via `#request`', async () => { const url = 'http://example.com'; const {auth, scopes} = mockGCE(); scopes.push(createGetProjectIdNock()); @@ -2567,7 +2578,29 @@ describe('googleauth', () => { scopes.forEach(s => s.done()); }); - it('request() should make the request with auth header', async () => { + it('#fetch() should make the request with auth header', async () => { + const url = 'http://example.com'; + const data = {breakfast: 'coffee'}; + const keyFilename = './test/fixtures/external-account-cred.json'; + const scopes = mockGetAccessTokenAndProjectId(false); + scopes.push( + nock(url) + .get('/', undefined, { + reqheaders: { + authorization: `Bearer ${stsSuccessfulResponse.access_token}`, + }, + }) + .reply(200, data), + ); + + const auth = new GoogleAuth({keyFilename}); + const res = await auth.fetch(url); + + assert.deepStrictEqual(res.data, data); + scopes.forEach(s => s.done()); + }); + + it('#request() should make the request with auth header', async () => { const url = 'http://example.com'; const data = {breakfast: 'coffee'}; const keyFilename = './test/fixtures/external-account-cred.json'; From 440de519d5c48b00c83d3941004d1516f084d6ce Mon Sep 17 00:00:00 2001 From: d-goog <188102366+d-goog@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:06:13 -0700 Subject: [PATCH 610/662] feat: Normalize `GoogleAuth` from `JSONClient` to `AuthClient` (#1940) * feat: Normalize to `GoogleAuth` See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com> --- src/auth/googleauth.ts | 30 +++++++++---------------- src/index.ts | 12 ++++++---- test/test.googleauth.ts | 50 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index b4d6d04f..a17ede2e 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -26,17 +26,8 @@ import {CredentialBody, ImpersonatedJWTInput, JWTInput} from './credentials'; import {IdTokenClient} from './idtokenclient'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; -import {OAuth2ClientOptions} from './oauth2client'; -import { - UserRefreshClient, - UserRefreshClientOptions, - USER_REFRESH_ACCOUNT_TYPE, -} from './refreshclient'; -import { - Impersonated, - ImpersonatedOptions, - IMPERSONATED_ACCOUNT_TYPE, -} from './impersonated'; +import {UserRefreshClient, USER_REFRESH_ACCOUNT_TYPE} from './refreshclient'; +import {Impersonated, IMPERSONATED_ACCOUNT_TYPE} from './impersonated'; import { ExternalAccountClient, ExternalAccountClientOptions, @@ -52,7 +43,7 @@ import { ExternalAccountAuthorizedUserClientOptions, } from './externalAccountAuthorizedUserClient'; import {originalOrCamelOptions} from '../util'; -import {AnyAuthClient} from '..'; +import {AnyAuthClient, AnyAuthClientConstructor} from '..'; /** * Defines all types of explicit clients that are determined via ADC JSON @@ -82,7 +73,7 @@ export interface ADCResponse { projectId: string | null; } -export interface GoogleAuthOptions { +export interface GoogleAuthOptions { /** * An API key to use, optional. Cannot be used with {@link GoogleAuthOptions.credentials `credentials`}. */ @@ -114,13 +105,12 @@ export interface GoogleAuthOptions { credentials?: JWTInput | ExternalAccountClientOptions; /** - * Options object passed to the constructor of the client + * `AuthClientOptions` object passed to the constructor of the client */ - clientOptions?: - | JWTOptions - | OAuth2ClientOptions - | UserRefreshClientOptions - | ImpersonatedOptions; + clientOptions?: Extract< + ConstructorParameters[0], + AuthClientOptions + >; /** * Required scopes for the desired API request @@ -160,7 +150,7 @@ export const GoogleAuthExceptionMessages = { 'https://cloud.google.com/compute/docs/metadata/predefined-metadata-keys', } as const; -export class GoogleAuth { +export class GoogleAuth { /** * Caches a value indicating whether the auth layer is running on Google * Compute Engine. diff --git a/src/index.ts b/src/index.ts index 5607d8be..b76585d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -91,12 +91,16 @@ export {PassThroughClient} from './auth/passthrough'; type ALL_EXPORTS = (typeof import('./'))[keyof typeof import('./')]; /** - * A union type for all {@link AuthClient `AuthClient`}s. + * A union type for all {@link AuthClient `AuthClient`} constructors. */ -export type AnyAuthClient = InstanceType< +export type AnyAuthClientConstructor = // Extract All `AuthClient`s from exports - Extract ->; + Extract; + +/** + * A union type for all {@link AuthClient `AuthClient`}s. + */ +export type AnyAuthClient = InstanceType; const auth = new GoogleAuth(); export {auth, GoogleAuth}; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index d203ee61..ddfd99d8 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -41,6 +41,8 @@ import { ExternalAccountClientOptions, Impersonated, IdentityPoolClient, + PassThroughClient, + AnyAuthClient, } from '../src'; import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; @@ -62,7 +64,7 @@ import {stringify} from 'querystring'; import {GoogleAuthExceptionMessages} from '../src/auth/googleauth'; import {IMPERSONATED_ACCOUNT_TYPE} from '../src/auth/impersonated'; import {USER_REFRESH_ACCOUNT_TYPE} from '../src/auth/refreshclient'; -import {Gaxios, GaxiosError} from 'gaxios'; +import {Gaxios, GaxiosError, GaxiosPromise, GaxiosResponse} from 'gaxios'; nock.disableNetConnect(); @@ -279,6 +281,52 @@ describe('googleauth', () => { return sandbox.stub(process, 'env').value(envVars); } + describe('types', () => { + it('should be type-compatible with itself by default', () => { + class CustomAuthClient extends AuthClient { + request(): GaxiosPromise { + throw new Error('Method not implemented.'); + } + getRequestHeaders(): Promise { + throw new Error('Method not implemented.'); + } + getAccessToken(): Promise<{ + token?: string | null; + res?: GaxiosResponse | null; + }> { + throw new Error('Method not implemented.'); + } + } + + // default > default + const defaultToDefault: GoogleAuth = new GoogleAuth(); + + // Explicit > default + const explicitToDefault: GoogleAuth = + new GoogleAuth(); + + // custom > default + const customToDefault: GoogleAuth = new GoogleAuth(); + + // AnyAuthClient > default + const anyToDefault: GoogleAuth = new GoogleAuth(); + + // default > AnyAuthClient + const defaultToAny: GoogleAuth = new GoogleAuth(); + + // AuthClient > AnyAuthClient + const baseClientToAny: GoogleAuth = + new GoogleAuth(); + + assert(defaultToDefault); + assert(explicitToDefault); + assert(customToDefault); + assert(anyToDefault); + assert(defaultToAny); + assert(baseClientToAny); + }); + }); + it('should accept and use an `AuthClient`', async () => { const customRequestHeaders = new Headers({ 'my-unique': 'header', From 082c171cae021e184d68bf3c794fdf8f2eddbd94 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:14:04 -0700 Subject: [PATCH 611/662] chore(main): release 10.1.0 (#2035) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e798ba01..6f8a771f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.1.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.0.0...v10.1.0) (2025-06-12) + + +### Features + +* `fetch`-Compatible API ([#1939](https://github.com/googleapis/google-auth-library-nodejs/issues/1939)) ([c63f608](https://github.com/googleapis/google-auth-library-nodejs/commit/c63f608ce33a3ea95b70f39eadcf0a4415e5657c)) +* Normalize `GoogleAuth` from `JSONClient` to `AuthClient` ([#1940](https://github.com/googleapis/google-auth-library-nodejs/issues/1940)) ([440de51](https://github.com/googleapis/google-auth-library-nodejs/commit/440de519d5c48b00c83d3941004d1516f084d6ce)) + + +### Bug Fixes + +* **deps:** Update dependency google-auth-library to v10 ([#2034](https://github.com/googleapis/google-auth-library-nodejs/issues/2034)) ([182c5db](https://github.com/googleapis/google-auth-library-nodejs/commit/182c5dba3ef9bcf9ae7eb2d3c7167d4fe899c118)) + ## [10.0.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v9.15.1...v10.0.0) (2025-06-11) diff --git a/package.json b/package.json index 34b3dc68..39bc3234 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.0.0", + "version": "10.1.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 229885fb..0bb85dba 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^28.0.0", - "google-auth-library": "^10.0.0", + "google-auth-library": "^10.1.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From bd7e4e3465567bba6f2599f63ac8ed62c0027de9 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:17:03 -0400 Subject: [PATCH 612/662] chore(owlbot-nodejs): install 3.13.5 Python (#2042) * chore: install higher version of Python * chore: update to python 3.15 * update lagging dependency * fix vulnerability * change the version Source-Link: https://github.com/googleapis/synthtool/commit/ca4c7ce65c001886c12b1c9b4ee216a7a1b807d2 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/issues-no-repro.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index f434738c..4b14618e 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:da69f1fd77b825b0520b1b0a047c270a3f7e3a42e4d46a5321376281cef6e62b -# created: 2025-06-02T21:06:54.667555755Z + digest: sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728 +# created: 2025-06-26T22:34:58.583582089Z diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 9b2f7014..816d9a70 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,7 +11,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install From 4915e97e56fe79fca1f974507b046c427d5c73e2 Mon Sep 17 00:00:00 2001 From: "Leah E. Cole" <6719667+leahecole@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:13:08 -0400 Subject: [PATCH 613/662] chore(deps): upgrade sinon to 21 (#2050) * chore(deps): upgrade sinon to 21 * specify which timers to fake * use @feywind's util for timers * add crucial file --- .github/scripts/package.json | 2 +- package.json | 2 +- test/test.awsclient.ts | 4 +- test/test.awsrequestsigner.ts | 4 +- test/test.baseexternalclient.ts | 15 ++--- test/test.downscopedclient.ts | 6 +- test/test.executableresponse.ts | 3 +- ...est.externalaccountauthorizeduserclient.ts | 3 +- test/test.pluggableauthclient.ts | 3 +- test/test.pluggableauthhandler.ts | 7 +-- test/test.util.ts | 3 +- test/utils.ts | 58 +++++++++++++++++++ 12 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 test/utils.ts diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 2c2e5207..26ab7802 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -16,6 +16,6 @@ "devDependencies": { "@octokit/rest": "^19.0.0", "mocha": "^10.0.0", - "sinon": "^18.0.0" + "sinon": "^21.0.0" } } \ No newline at end of file diff --git a/package.json b/package.json index 39bc3234..add9d90e 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "nock": "^14.0.1", "null-loader": "^4.0.0", "puppeteer": "^24.0.0", - "sinon": "^18.0.1", + "sinon": "^21.0.0", "ts-loader": "^8.0.0", "typescript": "^5.1.6", "webpack": "^5.21.2", diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index c9c5be41..4e7b6586 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -29,7 +29,7 @@ import { getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; import {AwsSecurityCredentials} from '../src/auth/awsrequestsigner'; - +import {TestUtils} from './utils'; nock.disableNetConnect(); const ONE_HOUR_IN_SECS = 3600; @@ -175,7 +175,7 @@ describe('AwsClient', () => { ); beforeEach(() => { - clock = sinon.useFakeTimers(referenceDate); + clock = TestUtils.useFakeTimers(sinon, referenceDate); }); afterEach(() => { diff --git a/test/test.awsrequestsigner.ts b/test/test.awsrequestsigner.ts index 7e3e4737..69ae3277 100644 --- a/test/test.awsrequestsigner.ts +++ b/test/test.awsrequestsigner.ts @@ -17,7 +17,7 @@ import {describe, it, afterEach, beforeEach} from 'mocha'; import * as sinon from 'sinon'; import {AwsRequestSigner} from '../src/auth/awsrequestsigner'; import {GaxiosOptions} from 'gaxios'; - +import {TestUtils} from './utils'; /** Defines the interface to facilitate testing of AWS request signing. */ interface AwsRequestSignerTest { // Test description. @@ -41,7 +41,7 @@ describe('AwsRequestSigner', () => { const token = awsSecurityCredentials.Token; beforeEach(() => { - clock = sinon.useFakeTimers(0); + clock = TestUtils.useFakeTimers(sinon); }); afterEach(() => { diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index e0820cba..c3f7444c 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -40,7 +40,7 @@ import { getExpectedExternalAccountMetricsHeaderValue, } from './externalclienthelper'; import {DEFAULT_UNIVERSE} from '../src/auth/authclient'; - +import {TestUtils} from './utils'; nock.disableNetConnect(); interface SampleResponse { @@ -965,7 +965,7 @@ describe('BaseExternalAccountClient', () => { }); it('should force refresh when cached credential is expired', async () => { - clock = sinon.useFakeTimers(0); + clock = TestUtils.useFakeTimers(sinon); const emittedEvents: Credentials[] = []; const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; @@ -1063,7 +1063,8 @@ describe('BaseExternalAccountClient', () => { }); it('should respect provided eagerRefreshThresholdMillis', async () => { - clock = sinon.useFakeTimers(0); + clock = TestUtils.useFakeTimers(sinon); + const customThresh = 10 * 1000; const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; @@ -1381,7 +1382,7 @@ describe('BaseExternalAccountClient', () => { }); it('should force refresh when cached credential is expired', async () => { - clock = sinon.useFakeTimers(0); + clock = TestUtils.useFakeTimers(sinon); const emittedEvents: Credentials[] = []; const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; @@ -1505,7 +1506,7 @@ describe('BaseExternalAccountClient', () => { }); it('should respect provided eagerRefreshThresholdMillis', async () => { - clock = sinon.useFakeTimers(0); + clock = TestUtils.useFakeTimers(sinon); const customThresh = 10 * 1000; const stsSuccessfulResponse2 = Object.assign({}, stsSuccessfulResponse); stsSuccessfulResponse2.access_token = 'ACCESS_TOKEN2'; @@ -2540,7 +2541,7 @@ describe('BaseExternalAccountClient', () => { describe('setCredentials()', () => { it('should allow injection of GCP access tokens directly', async () => { - clock = sinon.useFakeTimers(0); + clock = TestUtils.useFakeTimers(sinon); const credentials = { access_token: 'INJECTED_ACCESS_TOKEN', // Simulate token expires in 10mins. @@ -2581,7 +2582,7 @@ describe('BaseExternalAccountClient', () => { }); it('should not expire injected creds with no expiry_date', async () => { - clock = sinon.useFakeTimers(0); + clock = TestUtils.useFakeTimers(sinon); const credentials = { access_token: 'INJECTED_ACCESS_TOKEN', }; diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index 1cd259a9..2eec6094 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -32,7 +32,7 @@ import { getErrorFromOAuthErrorResponse, } from '../src/auth/oauth2common'; import {GetAccessTokenResponse} from '../src/auth/authclient'; - +import {TestUtils} from './utils'; nock.disableNetConnect(); /** A dummy class used as source credential for testing. */ @@ -376,7 +376,7 @@ describe('DownscopedClient', () => { describe('getAccessToken()', () => { it('should return current unexpired cached DownscopedClient access token', async () => { const now = new Date().getTime(); - clock = sinon.useFakeTimers(now); + clock = TestUtils.useFakeTimers(sinon, now); const credentials = { access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', expiry_date: now + ONE_HOUR_IN_SECS * 1000, @@ -415,7 +415,7 @@ describe('DownscopedClient', () => { it('should refresh a new DownscopedClient access when cached one gets expired', async () => { const now = new Date().getTime(); - clock = sinon.useFakeTimers(now); + clock = TestUtils.useFakeTimers(sinon, now); const emittedEvents: Credentials[] = []; const credentials = { access_token: 'DOWNSCOPED_CLIENT_ACCESS_TOKEN', diff --git a/test/test.executableresponse.ts b/test/test.executableresponse.ts index 778aa75a..2ab638c5 100644 --- a/test/test.executableresponse.ts +++ b/test/test.executableresponse.ts @@ -25,6 +25,7 @@ import { ExecutableResponseJson, } from '../src/auth/executable-response'; import * as sinon from 'sinon'; +import {TestUtils} from './utils'; const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2'; const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token'; @@ -35,7 +36,7 @@ describe('ExecutableResponse', () => { const referenceTime = 1653429377000; beforeEach(() => { - clock = sinon.useFakeTimers({now: referenceTime}); + clock = TestUtils.useFakeTimers(sinon, referenceTime); }); afterEach(() => { diff --git a/test/test.externalaccountauthorizeduserclient.ts b/test/test.externalaccountauthorizeduserclient.ts index 054d4307..e65aa213 100644 --- a/test/test.externalaccountauthorizeduserclient.ts +++ b/test/test.externalaccountauthorizeduserclient.ts @@ -30,6 +30,7 @@ import { OAuthErrorResponse, } from '../src/auth/oauth2common'; import {DEFAULT_UNIVERSE} from '../src/auth/authclient'; +import {TestUtils} from './utils'; nock.disableNetConnect(); @@ -110,7 +111,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { expires_in: 3600, }; beforeEach(() => { - clock = sinon.useFakeTimers(referenceDate); + clock = TestUtils.useFakeTimers(sinon, referenceDate); }); afterEach(() => { diff --git a/test/test.pluggableauthclient.ts b/test/test.pluggableauthclient.ts index 9b4abfc1..1a80007e 100644 --- a/test/test.pluggableauthclient.ts +++ b/test/test.pluggableauthclient.ts @@ -37,6 +37,7 @@ import { } from '../src/auth/executable-response'; import {PluggableAuthHandler} from '../src/auth/pluggable-auth-handler'; import {StsSuccessfulResponse} from '../src/auth/stscredentials'; +import {TestUtils} from './utils'; const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token'; const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2'; @@ -113,7 +114,7 @@ describe('PluggableAuthClient', () => { GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES: '1', }); sandbox.stub(process, 'env').value(envVars); - clock = sinon.useFakeTimers({now: referenceTime}); + clock = TestUtils.useFakeTimers(sinon, referenceTime); responseJson = { success: true, diff --git a/test/test.pluggableauthhandler.ts b/test/test.pluggableauthhandler.ts index ad442887..016d4a06 100644 --- a/test/test.pluggableauthhandler.ts +++ b/test/test.pluggableauthhandler.ts @@ -30,6 +30,7 @@ import { } from '../src/auth/pluggable-auth-handler'; import * as assert from 'assert'; import {ExecutableError} from '../src/auth/pluggable-auth-client'; +import {TestUtils} from './utils'; const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token'; const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2'; @@ -121,8 +122,7 @@ describe('PluggableAuthHandler', () => { beforeEach(() => { // Stub environment variables sandbox.stub(process, 'env').value(process.env); - clock = sandbox.useFakeTimers({now: referenceTime}); - + clock = TestUtils.useFakeTimers(sinon, referenceTime); defaultResponseJson = { success: true, version: 1, @@ -371,8 +371,7 @@ describe('PluggableAuthHandler', () => { let defaultResponseJson: ExecutableResponseJson; beforeEach(() => { - clock = sandbox.useFakeTimers({now: referenceTime}); - + clock = TestUtils.useFakeTimers(sinon, referenceTime); defaultResponseJson = { success: true, version: 1, diff --git a/test/test.util.ts b/test/test.util.ts index d167ba5b..a20a8a15 100644 --- a/test/test.util.ts +++ b/test/test.util.ts @@ -16,6 +16,7 @@ import {strict as assert} from 'assert'; import * as sinon from 'sinon'; import {LRUCache, removeUndefinedValuesInObject} from '../src/util'; +import {TestUtils} from './utils'; describe('util', () => { let sandbox: sinon.SinonSandbox; @@ -61,7 +62,7 @@ describe('util', () => { it('should evict items older than a supplied `maxAge`', async () => { const maxAge = 50; - sandbox.clock = sinon.useFakeTimers(); + sandbox.clock = TestUtils.useFakeTimers(sandbox); const lru = new LRUCache({capacity: 5, maxAge}); diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 00000000..99a02458 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,58 @@ +// 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. + +import {SinonSandbox, SinonFakeTimers} from 'sinon'; + +type FakeTimersParam = Parameters[0]; +interface FakeTimerConfig { + now?: number | Date; + toFake?: string[]; +} + +/** + * Utilities for unit test code. + * + * @private + */ +export class TestUtils { + /** + * This helper should be used to enable fake timers for Sinon sandbox. + * sinon adds a timer to `nextTick` by default beginning in v19 + * manually specifying the timers like this replicates the behavior pre v19 + * + * @param sandbox The sandbox + * @param now An optional date to set for "now" + * @returns The clock object from useFakeTimers() + */ + static useFakeTimers( + sandbox: SinonSandbox, + now?: number | Date, + ): SinonFakeTimers { + const config: FakeTimerConfig = { + toFake: [ + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'Date', + ], + }; + if (now) { + config.now = now; + } + + // The types are screwy in useFakeTimers(). I'm just going to pick one. + return sandbox.useFakeTimers(config as FakeTimersParam); + } +} From b8adc26657eafb6e61622e0da0035e7e791df710 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 9 Jul 2025 20:39:49 +0200 Subject: [PATCH 614/662] fix(deps): update dependency @googleapis/iam to v30 (#2052) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 0bb85dba..b1cd3c49 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^28.0.0", + "@googleapis/iam": "^30.0.0", "google-auth-library": "^10.1.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From be4ce78b2bc7ebc4df5bb22805c6a064f9ddb347 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:54:11 -0400 Subject: [PATCH 615/662] chore: add node 24 in node ci test (#2051) Source-Link: https://github.com/googleapis/synthtool/commit/1218bc231201438192c962136303b95f0a11a4f5 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:66c44f0ad8f6caaa4eb3fbe74f8c2b4de5a97c2b930cee069e712c447723ba95 Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 4b14618e..2a0311b8 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728 -# created: 2025-06-26T22:34:58.583582089Z + digest: sha256:66c44f0ad8f6caaa4eb3fbe74f8c2b4de5a97c2b930cee069e712c447723ba95 +# created: 2025-07-08T20:57:17.642848562Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 883082c0..ba80cb2c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [18, 20, 22] + node: [18, 20, 22, 24] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From 6ac9ab4fd49d64d8315f16d7f2757da04fbeb579 Mon Sep 17 00:00:00 2001 From: werman Date: Fri, 18 Jul 2025 11:39:59 -0700 Subject: [PATCH 616/662] feat: X509 cert authentication (#2055) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Implement X509 Cert Based Authentication (#2036) * chore(owlbot-nodejs): install 3.13.5 Python (#2042) * chore: install higher version of Python * chore: update to python 3.15 * update lagging dependency * fix vulnerability * change the version Source-Link: https://github.com/googleapis/synthtool/commit/ca4c7ce65c001886c12b1c9b4ee216a7a1b807d2 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728 Co-authored-by: Owl Bot * chore(deps): upgrade sinon to 21 (#2050) * chore(deps): upgrade sinon to 21 * specify which timers to fake * use @feywind's util for timers * add crucial file * fix(deps): update dependency @googleapis/iam to v30 (#2052) * chore: add node 24 in node ci test (#2051) Source-Link: https://github.com/googleapis/synthtool/commit/1218bc231201438192c962136303b95f0a11a4f5 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:66c44f0ad8f6caaa4eb3fbe74f8c2b4de5a97c2b930cee069e712c447723ba95 Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> * Included initial interfaces and options for creating x509client. * Added implementation for x509provider * Augmented logic for well-known cert config. * Added changes to create CertificateSubjectTokenSupplier * Added feature to call STS endpoint with the leaf certificate as trust chain. * Added logic to use trust chains. * Cleaned up certificateSubjectTokenSupplier and added mtlsClientTransporter logic to IdentityPoolClients Transporter * Added tests for certificateConfig type externalClient * All x509 auth logic in src/auth/certificatesubjecttokensupplier.ts * Added tests for malformed cert_config file, malfor med certificate in trust chain. * Added unit tests for util --------- Co-authored-by: gcf-owl-bot[bot] <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> Co-authored-by: Mend Renovate * feat: Async X509 file Operations (#2054) * chore(owlbot-nodejs): install 3.13.5 Python (#2042) * chore: install higher version of Python * chore: update to python 3.15 * update lagging dependency * fix vulnerability * change the version Source-Link: https://github.com/googleapis/synthtool/commit/ca4c7ce65c001886c12b1c9b4ee216a7a1b807d2 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728 Co-authored-by: Owl Bot * chore: add node 24 in node ci test (#2051) Source-Link: https://github.com/googleapis/synthtool/commit/1218bc231201438192c962136303b95f0a11a4f5 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:66c44f0ad8f6caaa4eb3fbe74f8c2b4de5a97c2b930cee069e712c447723ba95 Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> * X509 Cert Auth now does only async file reads * Fixed any linter error in util --------- Co-authored-by: gcf-owl-bot[bot] <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> * feat: x509 async Readme Update (#2056) * Added readme changes. * Addressed PR comments. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Readme changes transferred from Readme.md to readme-partials.yaml for Yoshi bot compliance * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: gcf-owl-bot[bot] <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> Co-authored-by: Mend Renovate --- .readme-partials.yaml | 59 +++ README.md | 59 +++ src/auth/baseexternalclient.ts | 19 +- src/auth/certificatesubjecttokensupplier.ts | 330 ++++++++++++ src/auth/identitypoolclient.ts | 89 +++- src/auth/oauth2client.ts | 4 +- src/auth/stscredentials.ts | 4 +- src/util.ts | 53 +- test/externalclienthelper.ts | 5 + .../external-account-cert/cert_config.json | 8 + .../cert_config_empty.json | 0 .../cert_config_missing_cert_path.json | 7 + .../cert_config_missing_key_path.json | 7 + .../cert_config_with_malformed_key.json | 8 + .../cert_config_with_malformed_leaf_cert.json | 8 + .../chain_with_leaf_middle.pem | 52 ++ .../chain_with_leaf_top.pem | 52 ++ .../chain_with_malformed_cert.pem | 23 + .../chain_with_no_leaf.pem | 53 ++ test/fixtures/external-account-cert/leaf.crt | 19 + test/fixtures/external-account-cert/leaf.key | 16 + .../external-account-cert/leaf_malformed.crt | 3 + .../external-account-cert/leaf_malformed.key | 3 + test/test.identitypoolclient.ts | 471 +++++++++++++++++- test/test.util.ts | 22 +- 25 files changed, 1346 insertions(+), 28 deletions(-) create mode 100644 src/auth/certificatesubjecttokensupplier.ts create mode 100644 test/fixtures/external-account-cert/cert_config.json create mode 100644 test/fixtures/external-account-cert/cert_config_empty.json create mode 100644 test/fixtures/external-account-cert/cert_config_missing_cert_path.json create mode 100644 test/fixtures/external-account-cert/cert_config_missing_key_path.json create mode 100644 test/fixtures/external-account-cert/cert_config_with_malformed_key.json create mode 100644 test/fixtures/external-account-cert/cert_config_with_malformed_leaf_cert.json create mode 100644 test/fixtures/external-account-cert/chain_with_leaf_middle.pem create mode 100644 test/fixtures/external-account-cert/chain_with_leaf_top.pem create mode 100644 test/fixtures/external-account-cert/chain_with_malformed_cert.pem create mode 100644 test/fixtures/external-account-cert/chain_with_no_leaf.pem create mode 100644 test/fixtures/external-account-cert/leaf.crt create mode 100644 test/fixtures/external-account-cert/leaf.key create mode 100644 test/fixtures/external-account-cert/leaf_malformed.crt create mode 100644 test/fixtures/external-account-cert/leaf_malformed.key diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 514b849a..8473de92 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -542,6 +542,65 @@ body: |- - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. + ### X.509 certificate-sourced credentials + For [X.509 certificate-sourced credentials](https://cloud.google.com/iam/docs/workload-identity-federation-with-x509-certificates), we use the certificate and private key cryptographic pair used to prove your application's identity. The certificate has a built-in expiration date and must be renewed before that date to maintain access. + + **Generating Configuration Files for X.509 Federation** + + To configure X.509 certificate-sourced credentials, you must generate two separate configuration files: a primary **credential configuration file** and a **certificate configuration file**. The `gcloud iam workload-identity-pools create-cred-config` command handles the creation of both. + + The location where the certificate configuration file is created depends on whether you use the `--credential-cert-configuration-output-file` flag. + + **Default Behavior (Recommended)** + + If you omit the `--credential-cert-configuration-output-file` flag, gcloud creates the certificate configuration file at a default, well-known location that client libraries can automatically discover. This is the simplest approach for most use cases. + + **Example Command (Default Behavior):** + ```bash + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --credential-cert-path "$PATH_TO_CERTIFICATE" \ + --credential-cert-private-key-path "$PATH_TO_PRIVATE_KEY" \ + --credential-cert-trust-chain-path "$PATH_TO_TRUST_CHAIN" \ + --output-file /path/to/config.json + ``` + the following variables need to be substituted: + - `$PROJECT_NUMBER`: The Google Cloud project number. + - `$POOL_ID`: The workload identity pool ID. + - `$PROVIDER_ID`: The provider ID. + - `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. + - `$PATH_TO_CERTIFICATE`: The file path where your leaf X.509 certificate is located. + - `$PATH_TO_PRIVATE_KEY`: The file path where the corresponding private key (.key) for the leaf certificate is located. + - `$PATH_TO_TRUST_CHAIN`: Points to the file path of the X.509 certificate trust chain file, containing any intermediate certificates required to complete the trust chain between the leaf certificate and the trust store configured in the Workload Identity Federation pool. + + This command results in: + - `/path/to/config.json`: Created at the path you specified. This file will contain `"use_default_certificate_config": true` to instruct clients to look for the certificate configuration at the default path. + - `certificate_config.json`: Created at the default gcloud configuration path, which is typically `~/.config/gcloud/certificate_config.json` on Linux and macOS, or `%APPDATA%\gcloud\certificate_config.json` on Windows. + + **Custom Location Behavior** + + If you need to store the certificate configuration file in a non-default location, use the `--credential-cert-configuration-output-file` flag. + + **Example Command (Custom Location):** + ```bash + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --credential-cert-path "$PATH_TO_CERTIFICATE" \ + --credential-cert-private-key-path "$PATH_TO_PRIVATE_KEY" \ + --credential-cert-trust-chain-path "$PATH_TO_TRUST_CHAIN" \ + --credential-cert-configuration-output-file "/custom/path/cert_config.json" \ + --output-file /path/to/config.json + ``` + + Use the default location example as a reference to substitute placeholders. + + This command results in: + + - `/path/to/config.json`: Created at the path you specified. This file will contain a `"certificate_config_location"` field that points to your custom path. + - `cert_config.json`: Created at `/custom/path/cert_config.json`, as specified by the flag. + ### Accessing resources from an OIDC or SAML2.0 identity provider using a custom supplier If you want to use OIDC or SAML2.0 that cannot be retrieved using methods supported natively by this library, diff --git a/README.md b/README.md index 17ee40cb..effaf66e 100644 --- a/README.md +++ b/README.md @@ -586,6 +586,65 @@ Where the following variables need to be substituted: - `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token. - `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`. +### X.509 certificate-sourced credentials +For [X.509 certificate-sourced credentials](https://cloud.google.com/iam/docs/workload-identity-federation-with-x509-certificates), we use the certificate and private key cryptographic pair used to prove your application's identity. The certificate has a built-in expiration date and must be renewed before that date to maintain access. + +**Generating Configuration Files for X.509 Federation** + +To configure X.509 certificate-sourced credentials, you must generate two separate configuration files: a primary **credential configuration file** and a **certificate configuration file**. The `gcloud iam workload-identity-pools create-cred-config` command handles the creation of both. + +The location where the certificate configuration file is created depends on whether you use the `--credential-cert-configuration-output-file` flag. + +**Default Behavior (Recommended)** + +If you omit the `--credential-cert-configuration-output-file` flag, gcloud creates the certificate configuration file at a default, well-known location that client libraries can automatically discover. This is the simplest approach for most use cases. + +**Example Command (Default Behavior):** +```bash +gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --credential-cert-path "$PATH_TO_CERTIFICATE" \ + --credential-cert-private-key-path "$PATH_TO_PRIVATE_KEY" \ + --credential-cert-trust-chain-path "$PATH_TO_TRUST_CHAIN" \ + --output-file /path/to/config.json +``` +the following variables need to be substituted: +- `$PROJECT_NUMBER`: The Google Cloud project number. +- `$POOL_ID`: The workload identity pool ID. +- `$PROVIDER_ID`: The provider ID. +- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. +- `$PATH_TO_CERTIFICATE`: The file path where your leaf X.509 certificate is located. +- `$PATH_TO_PRIVATE_KEY`: The file path where the corresponding private key (.key) for the leaf certificate is located. +- `$PATH_TO_TRUST_CHAIN`: Points to the file path of the X.509 certificate trust chain file, containing any intermediate certificates required to complete the trust chain between the leaf certificate and the trust store configured in the Workload Identity Federation pool. + +This command results in: +- `/path/to/config.json`: Created at the path you specified. This file will contain `"use_default_certificate_config": true` to instruct clients to look for the certificate configuration at the default path. +- `certificate_config.json`: Created at the default gcloud configuration path, which is typically `~/.config/gcloud/certificate_config.json` on Linux and macOS, or `%APPDATA%\gcloud\certificate_config.json` on Windows. + +**Custom Location Behavior** + +If you need to store the certificate configuration file in a non-default location, use the `--credential-cert-configuration-output-file` flag. + +**Example Command (Custom Location):** +```bash +gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --credential-cert-path "$PATH_TO_CERTIFICATE" \ + --credential-cert-private-key-path "$PATH_TO_PRIVATE_KEY" \ + --credential-cert-trust-chain-path "$PATH_TO_TRUST_CHAIN" \ + --credential-cert-configuration-output-file "/custom/path/cert_config.json" \ + --output-file /path/to/config.json +``` + +Use the default location example as a reference to substitute placeholders. + +This command results in: + +- `/path/to/config.json`: Created at the path you specified. This file will contain a `"certificate_config_location"` field that points to your custom path. +- `cert_config.json`: Created at `/custom/path/cert_config.json`, as specified by the flag. + ### Accessing resources from an OIDC or SAML2.0 identity provider using a custom supplier If you want to use OIDC or SAML2.0 that cannot be retrieved using methods supported natively by this library, diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index b1e7d61f..5c9196c4 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -229,17 +229,18 @@ export abstract class BaseExternalAccountClient extends AuthClient { * used. */ public scopes?: string | string[]; - private cachedAccessToken: CredentialsWithResponse | null; + public projectNumber: string | null; protected readonly audience: string; protected readonly subjectTokenType: string; + protected stsCredential: sts.StsCredentials; + protected readonly clientAuth?: ClientAuthentication; + protected credentialSourceType?: string; + private cachedAccessToken: CredentialsWithResponse | null; private readonly serviceAccountImpersonationUrl?: string; private readonly serviceAccountImpersonationLifetime?: number; - private readonly stsCredential: sts.StsCredentials; - private readonly clientAuth?: ClientAuthentication; private readonly workforcePoolUserProject?: string; - public projectNumber: string | null; private readonly configLifetimeRequested: boolean; - protected credentialSourceType?: string; + private readonly tokenUrl: string; /** * @example * ```ts @@ -281,7 +282,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { const clientId = opts.get('client_id'); const clientSecret = opts.get('client_secret'); - const tokenUrl = + this.tokenUrl = opts.get('token_url') ?? DEFAULT_TOKEN_URL.replace('{universeDomain}', this.universeDomain); const subjectTokenType = opts.get('subject_token_type'); @@ -310,7 +311,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { } this.stsCredential = new sts.StsCredentials({ - tokenExchangeEndpoint: tokenUrl, + tokenExchangeEndpoint: this.tokenUrl, clientAuthentication: this.clientAuth, }); this.scopes = opts.get('scopes') || [DEFAULT_OAUTH_SCOPE]; @@ -715,4 +716,8 @@ export abstract class BaseExternalAccountClient extends AuthClient { : 'unknown'; return `gl-node/${nodeVersion} auth/${pkg.version} google-byoid-sdk source/${credentialSourceType} sa-impersonation/${saImpersonation} config-lifetime/${this.configLifetimeRequested}`; } + + protected getTokenUrl(): string { + return this.tokenUrl; + } } diff --git a/src/auth/certificatesubjecttokensupplier.ts b/src/auth/certificatesubjecttokensupplier.ts new file mode 100644 index 00000000..d09382c3 --- /dev/null +++ b/src/auth/certificatesubjecttokensupplier.ts @@ -0,0 +1,330 @@ +// 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. + +import {SubjectTokenSupplier} from './identitypoolclient'; +import {getWellKnownCertificateConfigFileLocation, isValidFile} from '../util'; +import * as fs from 'fs'; +import {createPrivateKey, X509Certificate} from 'crypto'; +import * as https from 'https'; + +export const CERTIFICATE_CONFIGURATION_ENV_VARIABLE = + 'GOOGLE_API_CERTIFICATE_CONFIG'; + +/** + * Thrown when the certificate source cannot be located or accessed. + */ +export class CertificateSourceUnavailableError extends Error { + constructor(message: string) { + super(message); + this.name = 'CertificateSourceUnavailableError'; + } +} + +/** + * Thrown for invalid configuration that is not related to file availability. + */ +export class InvalidConfigurationError extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidConfigurationError'; + } +} + +/** + * Defines options for creating a {@link CertificateSubjectTokenSupplier}. + */ +export interface CertificateSubjectTokenSupplierOptions { + /** + * If true, uses the default well-known location for the certificate config. + * Either this or `certificateConfigLocation` must be provided. + */ + useDefaultCertificateConfig?: boolean; + /** + * The file path to the certificate configuration JSON file. + * Required if `useDefaultCertificateConfig` is not true. + */ + certificateConfigLocation?: string; + /** + * The file path to the trust chain (PEM format). + */ + trustChainPath?: string; +} + +/** + * Represents the "workload" block within the certificate configuration file. + * @internal + */ +interface WorkloadCertConfigJson { + cert_path: string; + key_path: string; +} + +/** + * Represents the structure of the certificate_config.json file. + * @internal + */ +interface CertificateConfigFileJson { + version: number; + cert_configs: { + workload?: WorkloadCertConfigJson; + }; +} + +/** + * A subject token supplier that uses a client certificate for authentication. + * It provides the certificate chain as the subject token for identity federation. + */ +export class CertificateSubjectTokenSupplier implements SubjectTokenSupplier { + private certificateConfigPath: string; + private readonly trustChainPath?: string; + private cert?: Buffer; + private key?: Buffer; + + /** + * Initializes a new instance of the CertificateSubjectTokenSupplier. + * @param opts The configuration options for the supplier. + */ + constructor(opts: CertificateSubjectTokenSupplierOptions) { + 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. + */ + public async createMtlsHttpsAgent(): Promise { + 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. + */ + public async getSubjectToken(): Promise { + // 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(): Promise { + // 1. Check for the override path from constructor options. + const overridePath = this.certificateConfigPath; + if (overridePath) { + if (await isValidFile(overridePath)) { + return overridePath; + } + throw new CertificateSourceUnavailableError( + `Provided certificate config path is invalid: ${overridePath}`, + ); + } + + // 2. Check the standard environment variable. + const envPath = process.env[CERTIFICATE_CONFIGURATION_ENV_VARIABLE]; + if (envPath) { + if (await isValidFile(envPath)) { + return envPath; + } + throw new CertificateSourceUnavailableError( + `Path from environment variable "${CERTIFICATE_CONFIGURATION_ENV_VARIABLE}" is invalid: ${envPath}`, + ); + } + + // 3. Check the well-known gcloud config location. + const wellKnownPath = getWellKnownCertificateConfigFileLocation(); + if (await 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 "${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(): Promise<{ + certPath: string; + keyPath: string; + }> { + const configPath = this.certificateConfigPath; + let fileContents: string; + 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) as CertificateConfigFileJson; + 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 as Error).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: string, + keyPath: string, + ): Promise<{ + cert: Buffer; + key: Buffer; + }> { + let cert, key; + try { + cert = await fs.promises.readFile(certPath); + new 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); + 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: Buffer): Promise { + const leafCert = new 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: X509Certificate[] = pemBlocks.map((pem, index) => { + try { + return new 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: X509Certificate[]; + + 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}`, + ); + } + } +} diff --git a/src/auth/identitypoolclient.ts b/src/auth/identitypoolclient.ts index bdc24ac7..743fc990 100644 --- a/src/auth/identitypoolclient.ts +++ b/src/auth/identitypoolclient.ts @@ -20,6 +20,9 @@ import { import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; import {FileSubjectTokenSupplier} from './filesubjecttokensupplier'; import {UrlSubjectTokenSupplier} from './urlsubjecttokensupplier'; +import {CertificateSubjectTokenSupplier} from './certificatesubjecttokensupplier'; +import {StsCredentials} from './stscredentials'; +import {Gaxios} from 'gaxios'; export type SubjectTokenFormatType = 'json' | 'text'; @@ -58,13 +61,13 @@ export interface IdentityPoolClientOptions */ credential_source?: { /** - * The file location to read the subject token from. Either this or a URL - * should be specified. + * The file location to read the subject token from. Either this, a URL + * or a certificate location should be specified. */ file?: string; /** - * The URL to call to retrieve the subject token. Either this or a file - * location should be specified. + * The URL to call to retrieve the subject token. Either this, a file + * location or a certificate location should be specified. */ url?: string; /** @@ -87,6 +90,42 @@ export interface IdentityPoolClientOptions */ subject_token_field_name?: string; }; + + /** + * The certificate location to call to retrieve the subject token. Either this, a file + * location, or an url should be specified. + * @example + * File Format: + * ```json + * { + * "cert_configs": { + * "workload": { + * "key_path": "$PATH_TO_LEAF_KEY", + * "cert_path": "$PATH_TO_LEAF_CERT" + * } + * } + * } + * ``` + */ + certificate?: { + /** + * Specify whether the certificate config should be used from the default location. + * Either this or the certificate_config_location must be provided. + * The certificate config file must be in the following JSON format: + */ + use_default_certificate_config?: boolean; + /** + * Location to fetch certificate config from in case default config is not to be used. + * Either this or use_default_certificate_config=true should be provided. + */ + certificate_config_location?: string; + /** + * TrustChainPath specifies the path to a PEM-formatted file containing the X.509 certificate trust chain. + * The file should contain any intermediate certificates needed to connect + * the mTLS leaf certificate to a root certificate in the trust store. + */ + trust_chain_path?: string; + }; }; /** * The subject token supplier to call to retrieve the subject token to exchange @@ -162,19 +201,20 @@ export class IdentityPoolClient extends BaseExternalAccountClient { const file = credentialSourceOpts.get('file'); const url = credentialSourceOpts.get('url'); + const certificate = credentialSourceOpts.get('certificate'); const headers = credentialSourceOpts.get('headers'); - if (file && url) { + if ((file && url) || (url && certificate) || (file && certificate)) { throw new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.', + 'No valid Identity Pool "credential_source" provided, must be either file, url, or certificate.', ); - } else if (file && !url) { + } else if (file) { this.credentialSourceType = 'file'; this.subjectTokenSupplier = new FileSubjectTokenSupplier({ filePath: file, formatType: formatType, subjectTokenFieldName: formatSubjectTokenFieldName, }); - } else if (!file && url) { + } else if (url) { this.credentialSourceType = 'url'; this.subjectTokenSupplier = new UrlSubjectTokenSupplier({ url: url, @@ -183,9 +223,19 @@ export class IdentityPoolClient extends BaseExternalAccountClient { headers: headers, additionalGaxiosOptions: IdentityPoolClient.RETRY_CONFIG, }); + } else if (certificate) { + this.credentialSourceType = 'certificate'; + const certificateSubjecttokensupplier = + new CertificateSubjectTokenSupplier({ + useDefaultCertificateConfig: + certificate.use_default_certificate_config, + certificateConfigLocation: certificate.certificate_config_location, + trustChainPath: certificate.trust_chain_path, + }); + this.subjectTokenSupplier = certificateSubjecttokensupplier; } else { throw new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.', + 'No valid Identity Pool "credential_source" provided, must be either file, url, or certificate.', ); } } @@ -198,6 +248,25 @@ export class IdentityPoolClient extends BaseExternalAccountClient { * @return A promise that resolves with the external subject token. */ async retrieveSubjectToken(): Promise { - return this.subjectTokenSupplier.getSubjectToken(this.supplierContext); + const subjectToken = await this.subjectTokenSupplier.getSubjectToken( + this.supplierContext, + ); + + if (this.subjectTokenSupplier instanceof CertificateSubjectTokenSupplier) { + const mtlsAgent = await this.subjectTokenSupplier.createMtlsHttpsAgent(); + + this.stsCredential = new StsCredentials({ + tokenExchangeEndpoint: this.getTokenUrl(), + clientAuthentication: this.clientAuth, + transporter: new Gaxios({agent: mtlsAgent}), + }); + + this.transporter = new Gaxios({ + ...(this.transporter.defaults || {}), + agent: mtlsAgent, + }); + } + + return subjectToken; } } diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 99e0398d..138e66c4 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -821,7 +821,9 @@ export class OAuth2Client extends AuthClient { ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, - data: new URLSearchParams(removeUndefinedValuesInObject(data)), + data: new URLSearchParams( + removeUndefinedValuesInObject(data) as Record, + ), }; AuthClient.setMethodName(opts, 'refreshTokenNoCache'); diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 0beaf3ca..625f7731 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -210,7 +210,9 @@ export class StsCredentials extends OAuthClientAuthHandler { url: this.#tokenExchangeEndpoint.toString(), method: 'POST', headers, - data: new URLSearchParams(removeUndefinedValuesInObject(values)), + data: new URLSearchParams( + removeUndefinedValuesInObject(values) as Record, + ), }; AuthClient.setMethodName(opts, 'exchangeToken'); diff --git a/src/util.ts b/src/util.ts index 99a01184..238ab604 100644 --- a/src/util.ts +++ b/src/util.ts @@ -12,6 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +import * as fs from 'fs'; +import * as os from 'os'; +import path = require('path'); + +const WELL_KNOWN_CERTIFICATE_CONFIG_FILE = 'certificate_config.json'; +const CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'; + /** * A utility for converting snake_case to camelCase. * @@ -241,8 +248,10 @@ export class LRUCache { } // Given and object remove fields where value is undefined. -export function removeUndefinedValuesInObject(object: {[key: string]: any}): { - [key: string]: any; +export function removeUndefinedValuesInObject(object: { + [key: string]: unknown; +}): { + [key: string]: unknown; } { Object.entries(object).forEach(([key, value]) => { if (value === undefined || value === 'undefined') { @@ -251,3 +260,43 @@ export function removeUndefinedValuesInObject(object: {[key: string]: any}): { }); return object; } + +/** + * Helper to check if a path points to a valid file. + */ +export async function isValidFile(filePath: string): Promise { + try { + const stats = await fs.promises.lstat(filePath); + return stats.isFile(); + } catch (e) { + return false; + } +} + +/** + * Determines the well-known gcloud location for the certificate config file. + * @returns The platform-specific path to the configuration file. + * @internal + */ +export function getWellKnownCertificateConfigFileLocation(): string { + const configDir = + process.env.CLOUDSDK_CONFIG || + (_isWindows() + ? path.join(process.env.APPDATA || '', CLOUDSDK_CONFIG_DIRECTORY) + : path.join( + process.env.HOME || '', + '.config', + CLOUDSDK_CONFIG_DIRECTORY, + )); + + return path.join(configDir, WELL_KNOWN_CERTIFICATE_CONFIG_FILE); +} + +/** + * Checks if the current operating system is Windows. + * @returns True if the OS is Windows, false otherwise. + * @internal + */ +function _isWindows(): boolean { + return os.platform().startsWith('win'); +} diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 09ca6ffa..f9517327 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -51,6 +51,7 @@ const defaultProjectNumber = '123456'; const poolId = 'POOL_ID'; const providerId = 'PROVIDER_ID'; const baseUrl = 'https://sts.googleapis.com'; +const baseMtlsUrl = 'https://sts.mtls.googleapis.com'; const path = '/v1/token'; export const saEmail = 'service-1234@service-name.iam.gserviceaccount.com'; const saBaseUrl = 'https://iamcredentials.googleapis.com'; @@ -112,6 +113,10 @@ export function getTokenUrl(): string { return `${baseUrl}${path}`; } +export function getMtlsTokenUrl(): string { + return `${baseMtlsUrl}${path}`; +} + export function getServiceAccountImpersonationUrl(): string { return `${saBaseUrl}${saPath}`; } diff --git a/test/fixtures/external-account-cert/cert_config.json b/test/fixtures/external-account-cert/cert_config.json new file mode 100644 index 00000000..b5e16c88 --- /dev/null +++ b/test/fixtures/external-account-cert/cert_config.json @@ -0,0 +1,8 @@ +{ + "cert_configs": { + "workload": { + "key_path": "./test/fixtures/external-account-cert/leaf.key", + "cert_path": "./test/fixtures/external-account-cert/leaf.crt" + } + } +} \ No newline at end of file diff --git a/test/fixtures/external-account-cert/cert_config_empty.json b/test/fixtures/external-account-cert/cert_config_empty.json new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/external-account-cert/cert_config_missing_cert_path.json b/test/fixtures/external-account-cert/cert_config_missing_cert_path.json new file mode 100644 index 00000000..a9607e7f --- /dev/null +++ b/test/fixtures/external-account-cert/cert_config_missing_cert_path.json @@ -0,0 +1,7 @@ +{ + "cert_configs": { + "workload": { + "key_path": "./test/fixtures/external-account-cert/leaf.key" + } + } +} \ No newline at end of file diff --git a/test/fixtures/external-account-cert/cert_config_missing_key_path.json b/test/fixtures/external-account-cert/cert_config_missing_key_path.json new file mode 100644 index 00000000..4dd9d40c --- /dev/null +++ b/test/fixtures/external-account-cert/cert_config_missing_key_path.json @@ -0,0 +1,7 @@ +{ + "cert_configs": { + "workload": { + "cert_path": "./test/fixtures/external-account-cert/leaf.crt" + } + } +} \ No newline at end of file diff --git a/test/fixtures/external-account-cert/cert_config_with_malformed_key.json b/test/fixtures/external-account-cert/cert_config_with_malformed_key.json new file mode 100644 index 00000000..c2321317 --- /dev/null +++ b/test/fixtures/external-account-cert/cert_config_with_malformed_key.json @@ -0,0 +1,8 @@ +{ + "cert_configs": { + "workload": { + "key_path": "./test/fixtures/external-account-cert/leaf_malformed.key", + "cert_path": "./test/fixtures/external-account-cert/leaf.crt" + } + } +} \ No newline at end of file diff --git a/test/fixtures/external-account-cert/cert_config_with_malformed_leaf_cert.json b/test/fixtures/external-account-cert/cert_config_with_malformed_leaf_cert.json new file mode 100644 index 00000000..e75e80e7 --- /dev/null +++ b/test/fixtures/external-account-cert/cert_config_with_malformed_leaf_cert.json @@ -0,0 +1,8 @@ +{ + "cert_configs": { + "workload": { + "key_path": "./test/fixtures/external-account-cert/leaf.key", + "cert_path": "./test/fixtures/external-account-cert/leaf_malformed.crt" + } + } +} \ No newline at end of file diff --git a/test/fixtures/external-account-cert/chain_with_leaf_middle.pem b/test/fixtures/external-account-cert/chain_with_leaf_middle.pem new file mode 100644 index 00000000..ab50d0d1 --- /dev/null +++ b/test/fixtures/external-account-cert/chain_with_leaf_middle.pem @@ -0,0 +1,52 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx +lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H +EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL +XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU +RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC +oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ +IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW +xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO +ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q +F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3 +uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F +mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw +bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX +riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS +6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh +CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl +sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR +pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N +vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv +/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi +pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7 +6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI +nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/ +lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV +BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV +MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM +7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer +uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp +gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 ++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 +ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O +gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh +GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr +odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk ++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 +ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql +ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT +cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/fixtures/external-account-cert/chain_with_leaf_top.pem b/test/fixtures/external-account-cert/chain_with_leaf_top.pem new file mode 100644 index 00000000..97b65f56 --- /dev/null +++ b/test/fixtures/external-account-cert/chain_with_leaf_top.pem @@ -0,0 +1,52 @@ +-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV +BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV +MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM +7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer +uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp +gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 ++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 +ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O +gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh +GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr +odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk ++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 +ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql +ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT +cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx +lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H +EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL +XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU +RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC +oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ +IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW +xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO +ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q +F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3 +uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F +mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw +bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX +riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS +6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh +CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl +sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR +pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N +vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv +/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi +pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7 +6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI +nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/ +lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/fixtures/external-account-cert/chain_with_malformed_cert.pem b/test/fixtures/external-account-cert/chain_with_malformed_cert.pem new file mode 100644 index 00000000..20a6212d --- /dev/null +++ b/test/fixtures/external-account-cert/chain_with_malformed_cert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +abbccssddeexx +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDPDCCAiSgAwIBAgIIFJsPvyc/ZSUwDQYJKoZIhvcNAQEFBQAwQTE/MD0GA1UE +AxM2aW50ZWdyYXRpb24tdGVzdHMuY2hpbmdvci10ZXN0LmlhbS5nc2VydmljZWFj +Y291bnQuY29tMB4XDTIwMDQwMjIyMjIxN1oXDTIyMDUwMTEzNTYxNVowQTE/MD0G +A1UEAxM2aW50ZWdyYXRpb24tdGVzdHMuY2hpbmdvci10ZXN0LmlhbS5nc2Vydmlj +ZWFjY291bnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Yys +P5LIa1rRxQY93FXIJDzq6Tai4VuetffJbltRtYbdwC5Vyl99O2zoVdRlg+iYXK5B +b6kidjmWOf0kNimQ5FwYvu+xsm6w8vjL/XShkHEKiURszyCua8wvLeGVCiGBg/XU +DOgYMjzRIH5fTuj3PTZk4sMj02ZCpCQEMQ6ogpLXjaLp3ZXtFhkuHyCxVYbTRr+k +GU86JAg4XwD6AdC349v+8FEQD7YtJezUAAKEgXh9e5UeL5CpOo3Vsdv/yEVo00jh +YuWzLM6Oxt55WAhiD29vKrm7VQPSr1XwwqpdyFL2BlmqyTlb3amwvc9qv2kojGvM +SUqgS83dc0jFqtMvEQIDAQABozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQE +AwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEA +m3XUMKOtXdpw0oRjykLHdYzTHHFjQTMmhWD172rsxSzwpkFoErAC7bnmEvpcRU7D +r4M+pE5VuDJ64J3lEpAh7W0zMXezPtGyWr39hVxL3vp3nh4CbCzzkUCfFvBOFFhm +OI9qnjtMtaozoGi5zLs5jEaFmgR3wfij9KQjNGZJxAg0ZkwcSNb76qOCG1/vG5au +4UuoIaq8WqSxMqBF/g+NrAE2PZhjNGnUwFPTre3SyR0otYDzJfmpL/tp5VDie8hM +L5UZU/CmZk46+T9VbvnZ5mkPAjGiPumiptO5iliBOHPtPdn8VrP+aSQM1btHA094 +1HwfbFp7pZHBUn9COAP/1Q== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/fixtures/external-account-cert/chain_with_no_leaf.pem b/test/fixtures/external-account-cert/chain_with_no_leaf.pem new file mode 100644 index 00000000..aab594f0 --- /dev/null +++ b/test/fixtures/external-account-cert/chain_with_no_leaf.pem @@ -0,0 +1,53 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx +lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H +EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL +XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU +RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC +oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ +IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW +xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO +ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q +F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3 +uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F +mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw +bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX +riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS +6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh +CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl +sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR +pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N +vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv +/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi +pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7 +6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI +nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/ +lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDPDCCAiSgAwIBAgIIFJsPvyc/ZSUwDQYJKoZIhvcNAQEFBQAwQTE/MD0GA1UE +AxM2aW50ZWdyYXRpb24tdGVzdHMuY2hpbmdvci10ZXN0LmlhbS5nc2VydmljZWFj +Y291bnQuY29tMB4XDTIwMDQwMjIyMjIxN1oXDTIyMDUwMTEzNTYxNVowQTE/MD0G +A1UEAxM2aW50ZWdyYXRpb24tdGVzdHMuY2hpbmdvci10ZXN0LmlhbS5nc2Vydmlj +ZWFjY291bnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Yys +P5LIa1rRxQY93FXIJDzq6Tai4VuetffJbltRtYbdwC5Vyl99O2zoVdRlg+iYXK5B +b6kidjmWOf0kNimQ5FwYvu+xsm6w8vjL/XShkHEKiURszyCua8wvLeGVCiGBg/XU +DOgYMjzRIH5fTuj3PTZk4sMj02ZCpCQEMQ6ogpLXjaLp3ZXtFhkuHyCxVYbTRr+k +GU86JAg4XwD6AdC349v+8FEQD7YtJezUAAKEgXh9e5UeL5CpOo3Vsdv/yEVo00jh +YuWzLM6Oxt55WAhiD29vKrm7VQPSr1XwwqpdyFL2BlmqyTlb3amwvc9qv2kojGvM +SUqgS83dc0jFqtMvEQIDAQABozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQE +AwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEA +m3XUMKOtXdpw0oRjykLHdYzTHHFjQTMmhWD172rsxSzwpkFoErAC7bnmEvpcRU7D +r4M+pE5VuDJ64J3lEpAh7W0zMXezPtGyWr39hVxL3vp3nh4CbCzzkUCfFvBOFFhm +OI9qnjtMtaozoGi5zLs5jEaFmgR3wfij9KQjNGZJxAg0ZkwcSNb76qOCG1/vG5au +4UuoIaq8WqSxMqBF/g+NrAE2PZhjNGnUwFPTre3SyR0otYDzJfmpL/tp5VDie8hM +L5UZU/CmZk46+T9VbvnZ5mkPAjGiPumiptO5iliBOHPtPdn8VrP+aSQM1btHA094 +1HwfbFp7pZHBUn9COAP/1Q== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/fixtures/external-account-cert/leaf.crt b/test/fixtures/external-account-cert/leaf.crt new file mode 100644 index 00000000..4219c297 --- /dev/null +++ b/test/fixtures/external-account-cert/leaf.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV +BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV +MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM +7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer +uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp +gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 ++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 +ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O +gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh +GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr +odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk ++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 +ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql +ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT +cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/fixtures/external-account-cert/leaf.key b/test/fixtures/external-account-cert/leaf.key new file mode 100644 index 00000000..d283ef5e --- /dev/null +++ b/test/fixtures/external-account-cert/leaf.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i +kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj +79Xc3yBDfKP2IeyYQIFe0t0zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kH +voa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw4Az2ZkmeuN6Fk/y9H+L +cb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB +/GrCtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ry +JR7GLbCzxPnJm/oQJBANwOCO6D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS +2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrPSXagBxzp8PecbaCHjz +NRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAk +AutLPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE +80L8jVLSbrbrlH43H0DjU5AkEAgidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEi +Ultk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJADj3e1YhMVdjJW5jq +wlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAz +MDjCQ== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/test/fixtures/external-account-cert/leaf_malformed.crt b/test/fixtures/external-account-cert/leaf_malformed.crt new file mode 100644 index 00000000..4a16756a --- /dev/null +++ b/test/fixtures/external-account-cert/leaf_malformed.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +fnksjdaksdakvbashjbvhjasdj +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/fixtures/external-account-cert/leaf_malformed.key b/test/fixtures/external-account-cert/leaf_malformed.key new file mode 100644 index 00000000..717e6ead --- /dev/null +++ b/test/fixtures/external-account-cert/leaf_malformed.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +udsbfjkryerkjnkdfjajdakjffasdasdvasjdhasfudsbjfdfbja +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 0c9a06ed..63ac15bd 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -32,7 +32,16 @@ import { mockGenerateAccessToken, mockStsTokenExchange, getExpectedExternalAccountMetricsHeaderValue, + getMtlsTokenUrl, } from './externalclienthelper'; +import {X509Certificate} from 'crypto'; +import { + CERTIFICATE_CONFIGURATION_ENV_VARIABLE, + CertificateSourceUnavailableError, + InvalidConfigurationError, +} from '../src/auth/certificatesubjecttokensupplier'; +import * as sinon from 'sinon'; +import * as util from '../src/util'; nock.disableNetConnect(); @@ -157,6 +166,26 @@ describe('IdentityPoolClient', () => { }, }, }; + const certSubjectToken = JSON.stringify([ + new X509Certificate( + fs.readFileSync( + './test/fixtures/external-account-cert/leaf.crt', + 'utf-8', + ), + ).raw.toString('base64'), + ]); + const certificateSourcedOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: + './test/fixtures/external-account-cert/cert_config.json', + }, + }, + }; const jsonRespUrlSourcedOptionsWithSA = Object.assign( { service_account_impersonation_url: getServiceAccountImpersonationUrl(), @@ -203,9 +232,9 @@ describe('IdentityPoolClient', () => { 'credentials.', ); - it('should throw when neither file or url sources are provided', () => { + it('should throw when neither file or url or certificate sources are provided', () => { const expectedError = new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.', + 'No valid Identity Pool "credential_source" provided, must be either file, url, or certificate.', ); const invalidOptions = { type: 'external_account', @@ -224,9 +253,9 @@ describe('IdentityPoolClient', () => { }, expectedError); }); - it('should throw when both file and url options are provided', () => { + it('should throw when more than 1 of file, url or certificate options are provided', () => { const expectedError = new Error( - 'No valid Identity Pool "credential_source" provided, must be either file or url.', + 'No valid Identity Pool "credential_source" provided, must be either file, url, or certificate.', ); const invalidOptions = { type: 'external_account', @@ -1396,6 +1425,440 @@ describe('IdentityPoolClient', () => { }); }); }); + + describe('for certificate-sourced subject tokens', () => { + const orgCertConfigVar = + process.env[CERTIFICATE_CONFIGURATION_ENV_VARIABLE]; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + // Restore the original value after each test case. + process.env[CERTIFICATE_CONFIGURATION_ENV_VARIABLE] = orgCertConfigVar; + sandbox.restore(); + }); + + describe('retrieveSubjectToken()', () => { + it('should resolve when a valid cert_config file is provided', async () => { + const client = new IdentityPoolClient(certificateSourcedOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, certSubjectToken); + }); + + it('should fail when neither default location is enabled not certificate config location is provided', async () => { + const expectedError = new InvalidConfigurationError( + 'Either `useDefaultCertificateConfig` must be true or a `certificateConfigLocation` must be provided.', + ); + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: {}, + }, + }; + assert.throws(() => { + new IdentityPoolClient(certificateSourcedOptionsWrong); + }, expectedError); + }); + + it('should fail when default location is enabled and certificate config location is provided', async () => { + const expectedError = new InvalidConfigurationError( + 'Both `useDefaultCertificateConfig` and `certificateConfigLocation` cannot be provided.', + ); + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + use_default_certificate_config: true, + certificate_config_location: + './test/fixtures/external-account-cert/cert_config.json', + }, + }, + }; + assert.throws(() => { + new IdentityPoolClient(certificateSourcedOptionsWrong); + }, expectedError); + }); + + it('should throw when invalid cert_config path is provided', async () => { + const overridePath = 'abc/efg'; + const expectedError = new CertificateSourceUnavailableError( + `Provided certificate config path is invalid: ${overridePath}`, + ); + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: overridePath, + }, + }, + }; + const client = new IdentityPoolClient(certificateSourcedOptionsWrong); + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should check GOOGLE_API_CERTIFICATE_CONFIG path for file', async () => { + process.env[CERTIFICATE_CONFIGURATION_ENV_VARIABLE] = + './test/fixtures/external-account-cert/cert_config.json'; + const certOptionsEnvVar: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + use_default_certificate_config: true, + }, + }, + }; + const client = new IdentityPoolClient(certOptionsEnvVar); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, certSubjectToken); + }); + + it('should throw in case default location is enabled and invalid GOOGLE_API_CERTIFICATE_CONFIG path', async () => { + const wrongPath = 'abc/efg'; + process.env[CERTIFICATE_CONFIGURATION_ENV_VARIABLE] = wrongPath; + const wrongCertOptionsEnvVar: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + use_default_certificate_config: true, + }, + }, + }; + const expectedError = new CertificateSourceUnavailableError( + `Path from environment variable "${CERTIFICATE_CONFIGURATION_ENV_VARIABLE}" is invalid: ${wrongPath}`, + ); + const client = new IdentityPoolClient(wrongCertOptionsEnvVar); + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should access well known certificate config location', async () => { + const mockPath = + './test/fixtures/external-account-cert/cert_config.json'; + process.env[CERTIFICATE_CONFIGURATION_ENV_VARIABLE] = ''; + const certOptionsDefault: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + use_default_certificate_config: true, + }, + }, + }; + const getLocationStub = sandbox.stub( + util, + 'getWellKnownCertificateConfigFileLocation', + ); + getLocationStub.returns(mockPath); + const client = new IdentityPoolClient(certOptionsDefault); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, certSubjectToken); + }); + + it('should throw in case default location is enabled and well known location is invalid', async () => { + const wrongPath = 'abc/efg'; + process.env[CERTIFICATE_CONFIGURATION_ENV_VARIABLE] = ''; + const wrongCertOptionsEnvVar: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + use_default_certificate_config: true, + }, + }, + }; + const expectedError = new CertificateSourceUnavailableError( + 'Could not find certificate configuration file. Searched override path, ' + + `the "${CERTIFICATE_CONFIGURATION_ENV_VARIABLE}" env var, and the gcloud path (${wrongPath}).`, + ); + + const getLocationStub = sandbox.stub( + util, + 'getWellKnownCertificateConfigFileLocation', + ); + getLocationStub.returns(wrongPath); + const client = new IdentityPoolClient(wrongCertOptionsEnvVar); + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should throw in case cert config has missing key path', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config_missing_key_path.json'; + const expectedError = new InvalidConfigurationError( + `Certificate config file (${certConfigPath}) is missing required "cert_path" or "key_path" in the workload config.`, + ); + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + }, + }, + }; + const client = new IdentityPoolClient(certificateSourcedOptionsWrong); + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should throw in case cert config has missing cert path', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config_missing_cert_path.json'; + const expectedError = new InvalidConfigurationError( + `Certificate config file (${certConfigPath}) is missing required "cert_path" or "key_path" in the workload config.`, + ); + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + }, + }, + }; + + const client = new IdentityPoolClient(certificateSourcedOptionsWrong); + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should throw in case cert config is empty or malformed', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config_empty.json'; + const expectedError = new RegExp( + `Failed to parse certificate config from ${certConfigPath}`, + ); + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + }, + }, + }; + + const client = new IdentityPoolClient(certificateSourcedOptionsWrong); + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should throw if cert has invalid PEM format', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config_with_malformed_leaf_cert.json'; + const expectedError = new RegExp('Failed to read certificate file'); + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + }, + }, + }; + + const client = new IdentityPoolClient(certificateSourcedOptionsWrong); + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should throw if key has invalid private key format', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config_with_malformed_key.json'; + const expectedError = new RegExp('Failed to read private key file'); + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + }, + }, + }; + + const client = new IdentityPoolClient(certificateSourcedOptionsWrong); + await assert.rejects(client.retrieveSubjectToken(), expectedError); + }); + + it('should throw if trust chain path is invalid', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config.json'; + const certificateSourcedOptionsWrong: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + trust_chain_path: 'abc/efg', + }, + }, + }; + const client = new IdentityPoolClient(certificateSourcedOptionsWrong); + + await assert.rejects( + client.retrieveSubjectToken(), + CertificateSourceUnavailableError, + ); + }); + + it('should return subject token when leaf cert is on top of trust chain', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config.json'; + const trustChainPath = + './test/fixtures/external-account-cert/chain_with_leaf_top.pem'; + const certificateSourcedOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + trust_chain_path: trustChainPath, + }, + }, + }; + const chainPems = fs.readFileSync(trustChainPath, 'utf8'); + const chainCerts = + chainPems + .match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) + ?.map(pem => new X509Certificate(pem)) ?? []; + const expectedSubjectToken = JSON.stringify( + chainCerts.map(cert => cert.raw.toString('base64')), + ); + + const client = new IdentityPoolClient(certificateSourcedOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + }); + + it('should throw when leaf cert is in the middle of trust chain', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config.json'; + const trustChainPath = + './test/fixtures/external-account-cert/chain_with_leaf_middle.pem'; + const certificateSourcedOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + trust_chain_path: trustChainPath, + }, + }, + }; + const client = new IdentityPoolClient(certificateSourcedOptions); + + await assert.rejects( + client.retrieveSubjectToken(), + new RegExp( + 'Leaf certificate exists in the trust chain but is not the first entry', + ), + ); + }); + + it('should return subject token when leaf cert is not in trust chain', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config.json'; + const trustChainPath = + './test/fixtures/external-account-cert/chain_with_no_leaf.pem'; + const certificateSourcedOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + trust_chain_path: trustChainPath, + }, + }, + }; + const leafCert = new X509Certificate( + fs.readFileSync( + './test/fixtures/external-account-cert/leaf.crt', + 'utf8', + ), + ); + const chainPems = fs.readFileSync(trustChainPath, 'utf8'); + const chainCerts = + chainPems + .match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) + ?.map(pem => new X509Certificate(pem)) ?? []; + const expectedSubjectToken = JSON.stringify( + [leafCert, ...chainCerts].map(cert => cert.raw.toString('base64')), + ); + + const client = new IdentityPoolClient(certificateSourcedOptions); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + }); + + it('should throw when one or more certs in trust chain is malformed', async () => { + const certConfigPath = + './test/fixtures/external-account-cert/cert_config.json'; + const trustChainPath = + './test/fixtures/external-account-cert/chain_with_malformed_cert.pem'; + const certificateSourcedOptions: IdentityPoolClientOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:mtls', + token_url: getMtlsTokenUrl(), + credential_source: { + certificate: { + certificate_config_location: certConfigPath, + trust_chain_path: trustChainPath, + }, + }, + }; + const client = new IdentityPoolClient(certificateSourcedOptions); + + await assert.rejects( + client.retrieveSubjectToken(), + new RegExp( + `Failed to parse certificate at index 0 in trust chain file ${ + trustChainPath + }`, + ), + ); + }); + }); + }); }); interface TestSubjectTokenSupplierOptions { diff --git a/test/test.util.ts b/test/test.util.ts index a20a8a15..b992003a 100644 --- a/test/test.util.ts +++ b/test/test.util.ts @@ -15,7 +15,11 @@ import {strict as assert} from 'assert'; import * as sinon from 'sinon'; -import {LRUCache, removeUndefinedValuesInObject} from '../src/util'; +import { + isValidFile, + LRUCache, + removeUndefinedValuesInObject, +} from '../src/util'; import {TestUtils} from './utils'; describe('util', () => { @@ -81,11 +85,23 @@ describe('util', () => { assert.equal(lru.get('second'), undefined); }); }); + + describe('isValidFilePath', () => { + it('should return true when valid file path', async () => { + const isValidPath = await isValidFile('./test/fixtures/empty.json'); + assert.equal(isValidPath, true); + }); + + it('should return false when invalid file path', async () => { + const isValidPath = await isValidFile('abc/pqr'); + assert.equal(isValidPath, false); + }); + }); }); describe('util removeUndefinedValuesInObject', () => { it('remove undefined type values in object', () => { - const object: {[key: string]: any} = { + const object: {[key: string]: unknown} = { undefined: undefined, number: 1, }; @@ -94,7 +110,7 @@ describe('util removeUndefinedValuesInObject', () => { }); }); it('remove undefined string values in object', () => { - const object: {[key: string]: any} = { + const object: {[key: string]: unknown} = { undefined: 'undefined', number: 1, }; From 7ebb3c81dab2d735e6ac5b288a9bd6a116457497 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:49:29 -0400 Subject: [PATCH 617/662] chore(main): release 10.2.0 (#2053) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f8a771f..40e07e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.2.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.1.0...v10.2.0) (2025-07-18) + + +### Features + +* X509 cert authentication ([#2055](https://github.com/googleapis/google-auth-library-nodejs/issues/2055)) ([6ac9ab4](https://github.com/googleapis/google-auth-library-nodejs/commit/6ac9ab4fd49d64d8315f16d7f2757da04fbeb579)) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v30 ([#2052](https://github.com/googleapis/google-auth-library-nodejs/issues/2052)) ([b8adc26](https://github.com/googleapis/google-auth-library-nodejs/commit/b8adc26657eafb6e61622e0da0035e7e791df710)) + ## [10.1.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.0.0...v10.1.0) (2025-06-12) diff --git a/package.json b/package.json index add9d90e..04900e56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.1.0", + "version": "10.2.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index b1cd3c49..29f2accc 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^30.0.0", - "google-auth-library": "^10.1.0", + "google-auth-library": "^10.2.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From afc3bc8f000c2794df626419515e87017b0dd655 Mon Sep 17 00:00:00 2001 From: miguel Date: Fri, 1 Aug 2025 12:27:18 -0700 Subject: [PATCH 618/662] fix: pin nock and typescript dependencies to fix window.crypto.subtle.verify signature argument type (#2106) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 04900e56..9fb332b2 100644 --- a/package.json +++ b/package.json @@ -53,12 +53,12 @@ "mocha": "^11.1.0", "mv": "^2.1.1", "ncp": "^2.0.0", - "nock": "^14.0.1", + "nock": "14.0.5", "null-loader": "^4.0.0", "puppeteer": "^24.0.0", "sinon": "^21.0.0", "ts-loader": "^8.0.0", - "typescript": "^5.1.6", + "typescript": "5.8.2", "webpack": "^5.21.2", "webpack-cli": "^4.0.0" }, From 9e61ac89a4af6ea7a0e9d5dcd8c37714311b073d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:29:09 -0700 Subject: [PATCH 619/662] chore(main): release 10.2.1 (#2107) * chore(main): release 10.2.1 * fix: add node version command to test workflow run * fix: undo change --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: miguel --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e07e34..15c816f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.2.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.2.0...v10.2.1) (2025-08-01) + + +### Bug Fixes + +* Pin nock and typescript dependencies to fix window.crypto.subtle.verify signature argument type ([#2106](https://github.com/googleapis/google-auth-library-nodejs/issues/2106)) ([afc3bc8](https://github.com/googleapis/google-auth-library-nodejs/commit/afc3bc8f000c2794df626419515e87017b0dd655)) + ## [10.2.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.1.0...v10.2.0) (2025-07-18) diff --git a/package.json b/package.json index 9fb332b2..fb588a46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.2.0", + "version": "10.2.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 29f2accc..95dda07a 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^30.0.0", - "google-auth-library": "^10.2.0", + "google-auth-library": "^10.2.1", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From 346a20618004b9a1219b5ec52b52c42462a85990 Mon Sep 17 00:00:00 2001 From: sofisl <55454395+sofisl@users.noreply.github.com> Date: Mon, 11 Aug 2025 14:34:57 -0700 Subject: [PATCH 620/662] chore: unpin nock (#2108) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb588a46..3985ab7a 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "mocha": "^11.1.0", "mv": "^2.1.1", "ncp": "^2.0.0", - "nock": "14.0.5", + "nock": "^14.0.5", "null-loader": "^4.0.0", "puppeteer": "^24.0.0", "sinon": "^21.0.0", From 6e159478f2bb85b5ea8d830a90430d8485bcce6f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 12 Aug 2025 00:29:24 +0200 Subject: [PATCH 621/662] chore(deps): update dependency node to v22 (#2110) --- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/issues-no-repro.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ba80cb2c..ce3a6919 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: node --version - run: npm install --engine-strict working-directory: .github/scripts @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm install --engine-strict - run: npm test env: @@ -57,7 +57,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm install - run: npm run lint docs: @@ -66,7 +66,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm install - run: npm run docs - uses: JustinBeckwith/linkinator-action@v1 diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 816d9a70..6b836e01 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm install working-directory: ./.github/scripts - uses: actions/github-script@v7 From 3e9d098b4af0a0fcf706b84f20364a034a375185 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 12 Aug 2025 01:15:47 +0200 Subject: [PATCH 622/662] chore(deps): update actions/checkout action to v5 (#2111) --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/issues-no-repro.yaml | 2 +- .github/workflows/response.yaml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ce3a6919..35a2bb30 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [18, 20, 22, 24] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: test-script: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 22 @@ -43,7 +43,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 22 @@ -54,7 +54,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 22 @@ -63,7 +63,7 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 6b836e01..0f598789 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -10,7 +10,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index 6ed37326..e81a3603 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -13,7 +13,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/github-script@v7 with: script: | @@ -27,7 +27,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/github-script@v7 with: script: | From ea9564b09f891af73dcfe8ecf2651723f9247119 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:29:26 -0400 Subject: [PATCH 623/662] chore: update issues-no-repro.yaml (#2112) Source-Link: https://github.com/googleapis/synthtool/commit/fb751351e1ca63cb6baaa3ecc2722ea6a1625a7b Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:2069d88011f3616de15f4d6b5c9e0c5eebbb39dc18a918e493c356956e23ead5 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 18 +++++++++--------- .github/workflows/response.yaml | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 2a0311b8..5786ae0b 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:66c44f0ad8f6caaa4eb3fbe74f8c2b4de5a97c2b930cee069e712c447723ba95 -# created: 2025-07-08T20:57:17.642848562Z + digest: sha256:2069d88011f3616de15f4d6b5c9e0c5eebbb39dc18a918e493c356956e23ead5 +# created: 2025-08-13T01:17:03.353099594Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 35a2bb30..ba80cb2c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [18, 20, 22, 24] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -29,10 +29,10 @@ jobs: test-script: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: node --version - run: npm install --engine-strict working-directory: .github/scripts @@ -43,10 +43,10 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: npm install --engine-strict - run: npm test env: @@ -54,19 +54,19 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: npm install - run: npm run lint docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: npm install - run: npm run docs - uses: JustinBeckwith/linkinator-action@v1 diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index e81a3603..6ed37326 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -13,7 +13,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - uses: actions/github-script@v7 with: script: | @@ -27,7 +27,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - uses: actions/github-script@v7 with: script: | From 0fd6d806b6cf170e2d2ddac23f5f0e4f1e896127 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:38:33 -0400 Subject: [PATCH 624/662] chore: Update response.yaml workflow (#2113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Update response.yaml workflow Source-Link: https://github.com/googleapis/synthtool/commit/d47c856822d8952427121905e27e6415b95985e9 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:1053e41d7e29d0619500416721caffff36357aa3708074ebdfae024b38ef3a40 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/response.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 5786ae0b..ddd5ae0a 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:2069d88011f3616de15f4d6b5c9e0c5eebbb39dc18a918e493c356956e23ead5 -# created: 2025-08-13T01:17:03.353099594Z + digest: sha256:1053e41d7e29d0619500416721caffff36357aa3708074ebdfae024b38ef3a40 +# created: 2025-08-13T13:35:22.107446359Z diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index 6ed37326..e81a3603 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -13,7 +13,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/github-script@v7 with: script: | @@ -27,7 +27,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/github-script@v7 with: script: | From a8df3c665dea736d8658c74cc2c20a91878dbc61 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:46:36 -0400 Subject: [PATCH 625/662] chore: update ci.yaml template (#2114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update ci.yaml template Source-Link: https://github.com/googleapis/synthtool/commit/d08775b0f7d3d00aa0ed1826e0d7d7b09bad1723 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:1861c5e2c9e12678f64f68c4ef449759f80c64299eb35a5e3c916eca46b0d2c4 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index ddd5ae0a..4dd3dd25 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:1053e41d7e29d0619500416721caffff36357aa3708074ebdfae024b38ef3a40 -# created: 2025-08-13T13:35:22.107446359Z + digest: sha256:1861c5e2c9e12678f64f68c4ef449759f80c64299eb35a5e3c916eca46b0d2c4 +# created: 2025-08-13T15:27:11.205698327Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ba80cb2c..10c83fc2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [18, 20, 22, 24] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -29,7 +29,7 @@ jobs: test-script: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -43,7 +43,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -54,7 +54,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -63,7 +63,7 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 From 1f7a92995748e4e33739a28187d2814085deded2 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:25:43 -0400 Subject: [PATCH 626/662] chore: revert previous node-version template updates (#2115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: revert previous node-version template updates * chore: update ci.yaml template * chore: update ci.yaml template node version * chore: revert previous node-version template changes Source-Link: https://github.com/googleapis/synthtool/commit/55f9ecb12170497c84d485174b620acd445b0ff8 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:1d7c29870723d4d2e32870c6dcdc43d4aa93dcc6519272d025bfcf2ecd48f091 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/issues-no-repro.yaml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 4dd3dd25..be00544b 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:1861c5e2c9e12678f64f68c4ef449759f80c64299eb35a5e3c916eca46b0d2c4 -# created: 2025-08-13T15:27:11.205698327Z + digest: sha256:1d7c29870723d4d2e32870c6dcdc43d4aa93dcc6519272d025bfcf2ecd48f091 +# created: 2025-08-14T17:16:30.591542591Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 10c83fc2..35a2bb30 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: node --version - run: npm install --engine-strict working-directory: .github/scripts @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm install --engine-strict - run: npm test env: @@ -57,7 +57,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm install - run: npm run lint docs: @@ -66,7 +66,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm install - run: npm run docs - uses: JustinBeckwith/linkinator-action@v1 diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 0f598789..53105402 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: npm install working-directory: ./.github/scripts - uses: actions/github-script@v7 From b97321fe97a4e7f499ddc9c3d8af7097c55fe3c7 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:55:55 -0400 Subject: [PATCH 627/662] chore: revert previous node-version template changes in ci.yaml (#2117) chore: revert previous node-version template changes Source-Link: https://github.com/googleapis/synthtool/commit/4b09ede35df7a2eb40ce6fb00617b71841c68b04 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:b612d739b0533e56ba174526ca339f264b63e911c30d6f83f55b57c38cc6ad2a Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index be00544b..3037bc54 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:1d7c29870723d4d2e32870c6dcdc43d4aa93dcc6519272d025bfcf2ecd48f091 -# created: 2025-08-14T17:16:30.591542591Z + digest: sha256:b612d739b0533e56ba174526ca339f264b63e911c30d6f83f55b57c38cc6ad2a +# created: 2025-08-15T12:36:48.871481111Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 35a2bb30..10c83fc2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: node --version - run: npm install --engine-strict working-directory: .github/scripts @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: npm install --engine-strict - run: npm test env: @@ -57,7 +57,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: npm install - run: npm run lint docs: @@ -66,7 +66,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 18 - run: npm install - run: npm run docs - uses: JustinBeckwith/linkinator-action@v1 From b659124b0a5f0071dcf74b6b48b252d371fed97b Mon Sep 17 00:00:00 2001 From: Daniel-Aaron-Bloom <76709210+Daniel-Aaron-Bloom@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:57:19 -0700 Subject: [PATCH 628/662] feat: add detection for Cloud Run Jobs (#2120) Co-authored-by: Daniel Bloom --- src/auth/envDetect.ts | 7 +++++++ test/test.googleauth.ts | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/auth/envDetect.ts b/src/auth/envDetect.ts index 506be799..406ec23e 100644 --- a/src/auth/envDetect.ts +++ b/src/auth/envDetect.ts @@ -20,6 +20,7 @@ export enum GCPEnv { CLOUD_FUNCTIONS = 'CLOUD_FUNCTIONS', COMPUTE_ENGINE = 'COMPUTE_ENGINE', CLOUD_RUN = 'CLOUD_RUN', + CLOUD_RUN_JOBS = 'CLOUD_RUN_JOBS', NONE = 'NONE', } @@ -48,6 +49,8 @@ async function getEnvMemoized(): Promise { env = GCPEnv.KUBERNETES_ENGINE; } else if (isCloudRun()) { env = GCPEnv.CLOUD_RUN; + } else if (isCloudRunJob()) { + env = GCPEnv.CLOUD_RUN_JOBS; } else { env = GCPEnv.COMPUTE_ENGINE; } @@ -74,6 +77,10 @@ function isCloudRun() { return !!process.env.K_CONFIGURATION; } +function isCloudRunJob() { + return !!process.env.CLOUD_RUN_JOB; +} + async function isKubernetesEngine() { try { await gcpMetadata.instance('attributes/cluster-name'); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index ddfd99d8..f61696b4 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1449,6 +1449,14 @@ describe('googleauth', () => { assert.strictEqual(env, envDetect.GCPEnv.CLOUD_RUN); }); + it('should get the current environment if Cloud Run Jobs', async () => { + envDetect.clear(); + mockEnvVar('CLOUD_RUN_JOB', 'KITTY'); + const {auth} = mockGCE(); + const env = await auth.getEnv(); + assert.strictEqual(env, envDetect.GCPEnv.CLOUD_RUN_JOBS); + }); + it('should make the request via `#fetch`', async () => { const url = 'http://example.com'; const {auth, scopes} = mockGCE(); From 1ec6856b32303491dd51fcfa2e5b663bd8cfb343 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:04:42 -0400 Subject: [PATCH 629/662] chore(main): release 10.3.0 (#2122) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15c816f4..ba1af8bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.3.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.2.1...v10.3.0) (2025-08-25) + + +### Features + +* Add detection for Cloud Run Jobs ([#2120](https://github.com/googleapis/google-auth-library-nodejs/issues/2120)) ([b659124](https://github.com/googleapis/google-auth-library-nodejs/commit/b659124b0a5f0071dcf74b6b48b252d371fed97b)) + ## [10.2.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.2.0...v10.2.1) (2025-08-01) diff --git a/package.json b/package.json index 3985ab7a..442a7112 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.2.1", + "version": "10.3.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 95dda07a..42e245ae 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^30.0.0", - "google-auth-library": "^10.2.1", + "google-auth-library": "^10.3.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From 71e5be902c1596eeccd52b8ea81553aa8c50571a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 25 Aug 2025 22:15:04 +0200 Subject: [PATCH 630/662] chore(deps): update dependency chai to v6 (#2121) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 42e245ae..f0f03df5 100644 --- a/samples/package.json +++ b/samples/package.json @@ -22,7 +22,7 @@ "server-destroy": "^1.0.1" }, "devDependencies": { - "chai": "^4.2.0", + "chai": "^6.0.0", "mocha": "^8.0.0" } } From 4f097fbcbee96401386e60a7a5be6d15e4fea69b Mon Sep 17 00:00:00 2001 From: "Leah E. Cole" <6719667+leahecole@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:44:40 -0400 Subject: [PATCH 631/662] Revert "chore(deps): update dependency chai to v6 (#2121)" (#2126) This reverts commit 71e5be902c1596eeccd52b8ea81553aa8c50571a. --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index f0f03df5..42e245ae 100644 --- a/samples/package.json +++ b/samples/package.json @@ -22,7 +22,7 @@ "server-destroy": "^1.0.1" }, "devDependencies": { - "chai": "^6.0.0", + "chai": "^4.2.0", "mocha": "^8.0.0" } } From 26fec2c0afefca7460002462733a9677806f9e6a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 9 Sep 2025 16:49:44 +0200 Subject: [PATCH 632/662] chore(deps): update actions/github-script action to v8 (#2124) Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .github/workflows/issues-no-repro.yaml | 2 +- .github/workflows/response.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 53105402..326400a9 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -16,7 +16,7 @@ jobs: node-version: 18 - run: npm install working-directory: ./.github/scripts - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/close-invalid-link.cjs') diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index e81a3603..38e987f7 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/close-unresponsive.cjs') @@ -28,7 +28,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/remove-response-label.cjs') From ce42458173fb0c96c3d5a29857f75f009e17ad63 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 9 Sep 2025 16:56:33 +0200 Subject: [PATCH 633/662] chore(deps): update actions/setup-node action to v5 (#2125) Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/issues-no-repro.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 10c83fc2..cd4bedfd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: node: [18, 20, 22, 24] steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} - run: node --version @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: node --version @@ -44,7 +44,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: npm install --engine-strict @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: npm install @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: npm install diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 326400a9..c176ecfe 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,7 +11,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: npm install From 7f66dd10e843215d722f0bb5bdaec32b3294c798 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:06:16 -0400 Subject: [PATCH 634/662] chore: update python post processor base image (#2128) Source-Link: https://github.com/googleapis/synthtool/commit/fe7743817ba2dced52578706639ba194966fba4e Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:84adf917cad8f48c61227febebae7af619882d7c8863d6ab6290a77d45a372cf Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/issues-no-repro.yaml | 4 ++-- .github/workflows/response.yaml | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 3037bc54..50bc3b4e 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:b612d739b0533e56ba174526ca339f264b63e911c30d6f83f55b57c38cc6ad2a -# created: 2025-08-15T12:36:48.871481111Z + digest: sha256:84adf917cad8f48c61227febebae7af619882d7c8863d6ab6290a77d45a372cf +# created: 2025-09-10T20:42:34.536728816Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cd4bedfd..10c83fc2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: node: [18, 20, 22, 24] steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: node --version @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: node --version @@ -44,7 +44,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install --engine-strict @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index c176ecfe..53105402 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,12 +11,12 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install working-directory: ./.github/scripts - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/close-invalid-link.cjs') diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index 38e987f7..e81a3603 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/close-unresponsive.cjs') @@ -28,7 +28,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/remove-response-label.cjs') From dc9f5cd04a53f40edfd8cfe652db0691157dda32 Mon Sep 17 00:00:00 2001 From: Gautam Sharda <57648023+GautamSharda@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:27:40 +0000 Subject: [PATCH 635/662] chore: deprecate unsafe loads & client options, add warnings to risky methods (#2134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deprecate unsafe methods, add warnings to risky credential types, and update README instructions * remove message added in prev commit; it is not strictly required here. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix(docs): update README with secure credential loading example Updates .readme-partials.yaml with the secure JWT constructor example for loading credentials from environment variables. This resolves a Windy Eagle vulnerability mitigation concern and adheres to the synthtool workflow for documentation. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * deprecate unsafe client options --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 13 ++-- README.md | 13 ++-- src/auth/externalclient.ts | 8 +++ src/auth/googleauth.ts | 130 +++++++++++++++++++++++++++++++++++-- src/auth/impersonated.ts | 7 ++ 5 files changed, 155 insertions(+), 16 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 8473de92..1f1f4b51 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -253,7 +253,7 @@ body: |- The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js). #### Loading credentials from environment variables - Instead of loading credentials from a key file, you can also provide them using an environment variable and the `GoogleAuth.fromJSON()` method. This is particularly convenient for systems that deploy directly from source control (Heroku, App Engine, etc). + Instead of loading credentials from a key file, you can also provide them using an environment variable. This is particularly convenient for systems that deploy directly from source control (Heroku, App Engine, etc). Start by exporting your credentials: @@ -274,7 +274,7 @@ body: |- Now you can create a new client from the credentials: ```js - const {auth} = require('google-auth-library'); + const {JWT} = require('google-auth-library'); // load the environment variable with our keys const keysEnvVar = process.env['CREDS']; @@ -283,9 +283,12 @@ body: |- } const keys = JSON.parse(keysEnvVar); - // load the JWT or UserRefreshClient from the keys - const client = auth.fromJSON(keys); - client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; + // create a JWT client + const client = new JWT({ + email: keys.client_email, + key: keys.private_key, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; const res = await client.fetch(url); console.log(res.data); diff --git a/README.md b/README.md index effaf66e..eeac66b5 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ console.log(res.data); The parameters for the JWT auth client including how to use it with a `.pem` file are explained in [samples/jwt.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/jwt.js). #### Loading credentials from environment variables -Instead of loading credentials from a key file, you can also provide them using an environment variable and the `GoogleAuth.fromJSON()` method. This is particularly convenient for systems that deploy directly from source control (Heroku, App Engine, etc). +Instead of loading credentials from a key file, you can also provide them using an environment variable. This is particularly convenient for systems that deploy directly from source control (Heroku, App Engine, etc). Start by exporting your credentials: @@ -318,7 +318,7 @@ $ export CREDS='{ Now you can create a new client from the credentials: ```js -const {auth} = require('google-auth-library'); +const {JWT} = require('google-auth-library'); // load the environment variable with our keys const keysEnvVar = process.env['CREDS']; @@ -327,9 +327,12 @@ if (!keysEnvVar) { } const keys = JSON.parse(keysEnvVar); -// load the JWT or UserRefreshClient from the keys -const client = auth.fromJSON(keys); -client.scopes = ['https://www.googleapis.com/auth/cloud-platform']; +// create a JWT client +const client = new JWT({ + email: keys.client_email, + key: keys.private_key, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], +}); const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`; const res = await client.fetch(url); console.log(res.data); diff --git a/src/auth/externalclient.ts b/src/auth/externalclient.ts index ea90a03c..f9a7cb0f 100644 --- a/src/auth/externalclient.ts +++ b/src/auth/externalclient.ts @@ -57,6 +57,14 @@ export class ExternalAccountClient { * This static method will instantiate the * corresponding type of external account credential depending on the * underlying credential source. + * + * **IMPORTANT**: This method does not validate the credential configuration. + * A security risk occurs when a credential configuration configured with + * malicious URLs is used. When the credential configuration is accepted from + * an untrusted source, you should validate it before using it with this + * method. For more details, see + * https://cloud.google.com/docs/authentication/external/externally-sourced-credentials. + * * @param options The external account options object typically loaded * from the external account JSON credential file. * @return A BaseExternalAccountClient instance or null if the options diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index a17ede2e..587233ec 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -84,7 +84,33 @@ export interface GoogleAuthOptions { */ authClient?: T; /** - * Path to a .json, .pem, or .p12 key file + * @deprecated This option is being deprecated because of a potential security risk. + * + * This option does not validate the credential configuration. The security + * risk occurs when a credential configuration is accepted from a source that + * is not under your control and used without validation on your side. + * + * The recommended way to provide credentials is to create an `auth` object + * using `google-auth-library` and pass it to the client constructor. + * This will ensure that unexpected credential types with potential for + * malicious intent are not loaded unintentionally. For example: + * ``` + * const {GoogleAuth} = require('google-auth-library'); + * const auth = new GoogleAuth({ + * // Scopes can be specified either as an array or as a single, space-delimited string. + * scopes: 'https://www.googleapis.com/auth/cloud-platform' + * }); + * const client = new MyClient({ auth: auth }); + * ``` + * + * If you are loading your credential configuration from an untrusted source and have + * not mitigated the risks (e.g. by validating the configuration yourself), make + * these changes as soon as possible to prevent security risks to your environment. + * + * Regardless of the method used, it is always your responsibility to validate + * configurations received from external sources. + * + * For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials. */ keyFilename?: string; @@ -94,13 +120,33 @@ export interface GoogleAuthOptions { keyFile?: string; /** - * Object containing client_email and private_key properties, or the - * external account client options. - * Cannot be used with {@link GoogleAuthOptions.apiKey `apiKey`}. + * @deprecated This option is being deprecated because of a potential security risk. * - * @remarks + * This option does not validate the credential configuration. The security + * risk occurs when a credential configuration is accepted from a source that + * is not under your control and used without validation on your side. * - * **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to {@link https://cloud.google.com/docs/authentication/external/externally-sourced-credentials Validate credential configurations from external sources}. + * The recommended way to provide credentials is to create an `auth` object + * using `google-auth-library` and pass it to the client constructor. + * This will ensure that unexpected credential types with potential for + * malicious intent are not loaded unintentionally. For example: + * ``` + * const {GoogleAuth} = require('google-auth-library'); + * const auth = new GoogleAuth({ + * // Scopes can be specified either as an array or as a single, space-delimited string. + * scopes: 'https://www.googleapis.com/auth/cloud-platform' + * }); + * const client = new MyClient({ auth: auth }); + * ``` + * + * If you are loading your credential configuration from an untrusted source and have + * not mitigated the risks (e.g. by validating the configuration yourself), make + * these changes as soon as possible to prevent security risks to your environment. + * + * Regardless of the method used, it is always your responsibility to validate + * configurations received from external sources. + * + * For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials. */ credentials?: JWTInput | ExternalAccountClientOptions; @@ -654,6 +700,38 @@ export class GoogleAuth { * * **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to {@link https://cloud.google.com/docs/authentication/external/externally-sourced-credentials Validate credential configurations from external sources}. * + * @deprecated This method is being deprecated because of a potential security risk. + * + * This method does not validate the credential configuration. The security + * risk occurs when a credential configuration is accepted from a source that + * is not under your control and used without validation on your side. + * + * If you know that you will be loading credential configurations of a + * specific type, it is recommended to use a credential-type-specific + * constructor. This will ensure that an unexpected credential type with + * potential for malicious intent is not loaded unintentionally. You might + * still have to do validation for certain credential types. Please follow + * the recommendation for that method. For example, if you want to load only + * service accounts, you can use the `JWT` constructor: + * ``` + * const {JWT} = require('google-auth-library'); + * const keys = require('/path/to/key.json'); + * const client = new JWT({ + * email: keys.client_email, + * key: keys.private_key, + * scopes: ['https://www.googleapis.com/auth/cloud-platform'], + * }); + * ``` + * + * If you are loading your credential configuration from an untrusted source and have + * not mitigated the risks (e.g. by validating the configuration yourself), make + * these changes as soon as possible to prevent security risks to your environment. + * + * Regardless of the method used, it is always your responsibility to validate + * configurations received from external sources. + * + * For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials. + * * @param json The input object. * @param options The JWT or UserRefresh options for the client * @returns JWT or UserRefresh Client with data @@ -719,6 +797,46 @@ export class GoogleAuth { /** * Create a credentials instance using the given input stream. + * + * @deprecated This method is being deprecated because of a potential security risk. + * + * This method does not validate the credential configuration. The security + * risk occurs when a credential configuration is accepted from a source that + * is not under your control and used without validation on your side. + * + * If you know that you will be loading credential configurations of a + * specific type, it is recommended to read and parse the stream, and then + * use a credential-type-specific constructor. This will ensure that an + * unexpected credential type with potential for malicious intent is not + * loaded unintentionally. You might still have to do validation for certain + * credential types. Please follow the recommendation for that method. For + * example, if you want to load only service accounts, you can do: + * ``` + * const {JWT} = require('google-auth-library'); + * const fs = require('fs'); + * + * const stream = fs.createReadStream('path/to/key.json'); + * const chunks = []; + * stream.on('data', (chunk) => chunks.push(chunk)); + * stream.on('end', () => { + * const keys = JSON.parse(Buffer.concat(chunks).toString()); + * const client = new JWT({ + * email: keys.client_email, + * key: keys.private_key, + * scopes: ['https://www.googleapis.com/auth/cloud-platform'], + * }); + * // use client + * }); + * ``` + * + * If you are loading your credential configuration from an untrusted source and have + * not mitigated the risks (e.g. by validating the configuration yourself), make + * these changes as soon as possible to prevent security risks to your environment. + * + * Regardless of the method used, it is always your responsibility to validate + * configurations received from external sources. + * + * For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials. * @param inputStream The input stream. * @param callback Optional callback. */ diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index 04d52cfb..97742ef6 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -91,6 +91,13 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { * Also, the target service account must grant the orginating principal * the "Service Account Token Creator" IAM role. * + * **IMPORTANT**: This method does not validate the credential configuration. + * A security risk occurs when a credential configuration configured with + * malicious URLs is used. When the credential configuration is accepted from + * an untrusted source, you should validate it before using it with this + * method. For more details, see + * https://cloud.google.com/docs/authentication/external/externally-sourced-credentials. + * * @param {object} options - The configuration object. * @param {object} [options.sourceClient] the source credential used as to * acquire the impersonated credentials. From 123e10ec21058878d3353eef412c1eb3780075b6 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 25 Sep 2025 22:45:00 +0200 Subject: [PATCH 636/662] chore(deps): update actions/github-script action to v8 (#2129) --- .github/workflows/issues-no-repro.yaml | 2 +- .github/workflows/response.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 53105402..326400a9 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -16,7 +16,7 @@ jobs: node-version: 18 - run: npm install working-directory: ./.github/scripts - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/close-invalid-link.cjs') diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index e81a3603..38e987f7 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/close-unresponsive.cjs') @@ -28,7 +28,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/remove-response-label.cjs') From 10817e2868d45cecc3a130945768032660e4fda7 Mon Sep 17 00:00:00 2001 From: Gautam Sharda <57648023+GautamSharda@users.noreply.github.com> Date: Thu, 25 Sep 2025 21:30:15 +0000 Subject: [PATCH 637/662] chore: deprecate unsafe client option (#2135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: deprecate unsafe client option * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .github/workflows/issues-no-repro.yaml | 2 +- .github/workflows/response.yaml | 4 ++-- src/auth/googleauth.ts | 28 +++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 326400a9..53105402 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -16,7 +16,7 @@ jobs: node-version: 18 - run: npm install working-directory: ./.github/scripts - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/close-invalid-link.cjs') diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index 38e987f7..e81a3603 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/close-unresponsive.cjs') @@ -28,7 +28,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/remove-response-label.cjs') diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 587233ec..f566a3b8 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -115,7 +115,33 @@ export interface GoogleAuthOptions { keyFilename?: string; /** - * Path to a .json, .pem, or .p12 key file + * @deprecated This option is being deprecated because of a potential security risk. + * + * This option does not validate the credential configuration. The security + * risk occurs when a credential configuration is accepted from a source that + * is not under your control and used without validation on your side. + * + * The recommended way to provide credentials is to create an `auth` object + * using `google-auth-library` and pass it to the client constructor. + * This will ensure that unexpected credential types with potential for + * malicious intent are not loaded unintentionally. For example: + * ``` + * const {GoogleAuth} = require('google-auth-library'); + * const auth = new GoogleAuth({ + * // Scopes can be specified either as an array or as a single, space-delimited string. + * scopes: 'https://www.googleapis.com/auth/cloud-platform' + * }); + * const client = new MyClient({ auth: auth }); + * ``` + * + * If you are loading your credential configuration from an untrusted source and have + * not mitigated the risks (e.g. by validating the configuration yourself), make + * these changes as soon as possible to prevent security risks to your environment. + * + * Regardless of the method used, it is always your responsibility to validate + * configurations received from external sources. + * + * For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials. */ keyFile?: string; From b375423a302ac604e01bdb10bf6298224b3e86c4 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 25 Sep 2025 23:44:15 +0200 Subject: [PATCH 638/662] chore(deps): update actions/setup-node action to v5 (#2130) Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> Co-authored-by: miguel --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/issues-no-repro.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 10c83fc2..cd4bedfd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: node: [18, 20, 22, 24] steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} - run: node --version @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: node --version @@ -44,7 +44,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: npm install --engine-strict @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: npm install @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: npm install diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 53105402..a83ff121 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,7 +11,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: npm install From 23c13c52017cc83b7d38d138bb997a052cd3235f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 25 Sep 2025 23:50:48 +0200 Subject: [PATCH 639/662] fix(deps): update dependency @googleapis/iam to v32 (#2133) Co-authored-by: miguel --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 42e245ae..903e166c 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,7 +15,7 @@ "dependencies": { "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", - "@googleapis/iam": "^30.0.0", + "@googleapis/iam": "^32.0.0", "google-auth-library": "^10.3.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From 07edd7a1a705887c244862f51c919ecb85260a52 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 26 Sep 2025 17:48:51 +0100 Subject: [PATCH 640/662] chore(deps): update actions/github-script action to v8 (#2137) --- .github/workflows/issues-no-repro.yaml | 2 +- .github/workflows/response.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index a83ff121..c176ecfe 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -16,7 +16,7 @@ jobs: node-version: 18 - run: npm install working-directory: ./.github/scripts - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/close-invalid-link.cjs') diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index e81a3603..38e987f7 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/close-unresponsive.cjs') @@ -28,7 +28,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/remove-response-label.cjs') From c2c446990050ce2182fe1845766d60c370cb199e Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:43:37 -0700 Subject: [PATCH 641/662] chore(main): release 10.3.1 (#2136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(main): release 10.3.1 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Owl Bot --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/issues-no-repro.yaml | 4 ++-- .github/workflows/response.yaml | 4 ++-- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cd4bedfd..10c83fc2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: node: [18, 20, 22, 24] steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: node --version @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: node --version @@ -44,7 +44,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install --engine-strict @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index c176ecfe..53105402 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,12 +11,12 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v4 with: node-version: 18 - run: npm install working-directory: ./.github/scripts - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/close-invalid-link.cjs') diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index 38e987f7..e81a3603 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/close-unresponsive.cjs') @@ -28,7 +28,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v8 + - uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/remove-response-label.cjs') diff --git a/CHANGELOG.md b/CHANGELOG.md index ba1af8bd..be00e79f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.3.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.3.0...v10.3.1) (2025-09-26) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v32 ([#2133](https://github.com/googleapis/google-auth-library-nodejs/issues/2133)) ([23c13c5](https://github.com/googleapis/google-auth-library-nodejs/commit/23c13c52017cc83b7d38d138bb997a052cd3235f)) + ## [10.3.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.2.1...v10.3.0) (2025-08-25) diff --git a/package.json b/package.json index 442a7112..43b2a4cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.3.0", + "version": "10.3.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 903e166c..b71de162 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^32.0.0", - "google-auth-library": "^10.3.0", + "google-auth-library": "^10.3.1", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From cae596bcf3de1376c57c2cf92a45a8aff8ddd593 Mon Sep 17 00:00:00 2001 From: Gautam Sharda <57648023+GautamSharda@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:52:53 -0700 Subject: [PATCH 642/662] =?UTF-8?q?feat:=20add=20console=20warnings=20for?= =?UTF-8?q?=20mitigating=20file=20based=20credential=20load=20=E2=80=A6=20?= =?UTF-8?q?(#2143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add console warnings for mitigating file based credential load related auth methods, options, and types * fix npm run lint trailing commas --- src/auth/externalclient.ts | 3 +++ src/auth/googleauth.ts | 16 ++++++++++++++++ src/auth/impersonated.ts | 3 +++ 3 files changed, 22 insertions(+) diff --git a/src/auth/externalclient.ts b/src/auth/externalclient.ts index f9a7cb0f..8157a62e 100644 --- a/src/auth/externalclient.ts +++ b/src/auth/externalclient.ts @@ -73,6 +73,9 @@ export class ExternalAccountClient { static fromJSON( options: ExternalAccountClientOptions, ): BaseExternalAccountClient | null { + console.warn( + 'The `fromJSON` method does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); if (options && options.type === EXTERNAL_ACCOUNT_TYPE) { if ((options as AwsClientOptions).credential_source?.environment_id) { return new AwsClient(options as AwsClientOptions); diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index f566a3b8..234dcdaa 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -275,9 +275,19 @@ export class GoogleAuth { this._cachedProjectId = opts.projectId || null; this.cachedCredential = opts.authClient || null; this.keyFilename = opts.keyFilename || opts.keyFile; + if (this.keyFilename) { + console.warn( + 'The `keyFilename` option is deprecated. Please use the `credentials` option instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); + } this.scopes = opts.scopes; this.clientOptions = opts.clientOptions || {}; this.jsonContent = opts.credentials || null; + if (this.jsonContent) { + console.warn( + 'The `credentials` option is deprecated. Please use the `auth` object constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); + } this.apiKey = opts.apiKey || this.clientOptions.apiKey || null; // Cannot use both API Key + Credentials @@ -766,6 +776,9 @@ export class GoogleAuth { json: JWTInput | ImpersonatedJWTInput, options: AuthClientOptions = {}, ): JSONClient { + console.warn( + 'The `fromJSON` method is deprecated. Please use the `JWT` constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); let client: JSONClient; // user's preferred universe domain @@ -882,6 +895,9 @@ export class GoogleAuth { optionsOrCallback: AuthClientOptions | CredentialCallback = {}, callback?: CredentialCallback, ): Promise | void { + console.warn( + 'The `fromStream` method is deprecated. Please use the `JWT` constructor with a parsed stream instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); let options: AuthClientOptions = {}; if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index 97742ef6..708b6a0f 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -121,6 +121,9 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { */ constructor(options: ImpersonatedOptions = {}) { super(options); + console.warn( + 'The `Impersonated` constructor does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); // Start with an expired refresh token, which will automatically be // refreshed before the first API call is made. this.credentials = { From 1d9054aa427456b4f3c13e06fcaea583074c704d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:16:19 -0700 Subject: [PATCH 643/662] chore(main): release 10.4.0 (#2144) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be00e79f..5c8b8a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.4.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.3.1...v10.4.0) (2025-09-30) + + +### Features + +* Add console warnings for mitigating file based credential load … ([#2143](https://github.com/googleapis/google-auth-library-nodejs/issues/2143)) ([cae596b](https://github.com/googleapis/google-auth-library-nodejs/commit/cae596bcf3de1376c57c2cf92a45a8aff8ddd593)) + ## [10.3.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.3.0...v10.3.1) (2025-09-26) diff --git a/package.json b/package.json index 43b2a4cb..c7378870 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.3.1", + "version": "10.4.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index b71de162..bd026b86 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@googleapis/iam": "^32.0.0", - "google-auth-library": "^10.3.1", + "google-auth-library": "^10.4.0", "node-fetch": "^2.3.0", "open": "^9.0.0", "server-destroy": "^1.0.1" From fce828cfed1c50746f4013bd43f3353ca5bd71fc Mon Sep 17 00:00:00 2001 From: werman Date: Tue, 30 Sep 2025 15:18:29 -0700 Subject: [PATCH 644/662] chore: Custom Credential Supplier Documentation (#2132) * chore: Custom Credential Supplier Documentation * Made some comment changes. * Included changes for Client Credentials Grant for Okta workforce. * Changed AwsWorkload to include caching. * Lint and package.json changes * Impersonation not needed. * Included changes to get region from sdk for customCredentialSupplierAwsWorkload. Got rid of samples/readme changes. * Removed customCredentialSupplier for workload. * Implemented OktaWorkload with client credential grant flow. AWS now leverages AWS-SDK level caching. * Reverted back to using gaxios to fetch okta access token --- .readme-partials.yaml | 4 + samples/.eslintrc.yml | 5 + .../customCredentialSupplierAwsWorkload.js | 134 +++++++++++++ .../customCredentialSupplierOktaWorkload.js | 181 ++++++++++++++++++ samples/package.json | 6 +- 5 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 samples/customCredentialSupplierAwsWorkload.js create mode 100644 samples/customCredentialSupplierOktaWorkload.js diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 1f1f4b51..0abe2208 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -379,6 +379,8 @@ body: |- Note that the client does not cache the returned AWS security credentials, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. + For a sample on how to access Google Cloud resources from AWS with a custom credential supplier, see [samples/customCredentialSupplierAwsWorkload.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierAwsWorkload.js). + ```ts import { AwsClient, AwsSecurityCredentials, AwsSecurityCredentialsSupplier, ExternalAccountSupplierContext } from 'google-auth-library'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; @@ -1059,6 +1061,8 @@ body: |- The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#use_configuration_files_for_sign-in). + For a sample on how to access Google Cloud resources from an Okta identity provider with a custom credential supplier, see [samples/customCredentialSupplierOktaWorkforce.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierOktaWorkforce.js). + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. diff --git a/samples/.eslintrc.yml b/samples/.eslintrc.yml index da24d6c7..3d3740d3 100644 --- a/samples/.eslintrc.yml +++ b/samples/.eslintrc.yml @@ -1,5 +1,10 @@ --- +parserOptions: + ecmaVersion: 2023 rules: no-console: off node/no-missing-require: off node/no-unpublished-require: off + no-unused-vars: + - error + - argsIgnorePattern: '^_' diff --git a/samples/customCredentialSupplierAwsWorkload.js b/samples/customCredentialSupplierAwsWorkload.js new file mode 100644 index 00000000..351ac308 --- /dev/null +++ b/samples/customCredentialSupplierAwsWorkload.js @@ -0,0 +1,134 @@ +// 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. + +('use strict'); +require('dotenv').config(); + +const {AwsClient} = require('google-auth-library'); +const {fromNodeProviderChain} = require('@aws-sdk/credential-providers'); +const {STSClient} = require('@aws-sdk/client-sts'); + +/** + * Custom AWS Security Credentials Supplier. + * + * This implementation resolves AWS credentials using the default Node provider + * chain from the AWS SDK. This allows fetching credentials from environment + * variables, shared credential files (~/.aws/credentials), or IAM roles + * for service accounts (IRSA) in EKS, etc. + */ +class CustomAwsSupplier { + constructor() { + // Will be cached upon first resolution. + this.region = null; + + // Initialize the AWS credential provider. + // The AWS SDK handles memoization (caching) and proactive refreshing internally. + this.awsCredentialsProvider = fromNodeProviderChain(); + } + + /** + * Returns the AWS region. This is required for signing the AWS request. + * It resolves the region automatically by using the default AWS region + * provider chain, which searches for the region in the standard locations + * (environment variables, AWS config file, etc.). + */ + async getAwsRegion(_context) { + if (this.region) { + return this.region; + } + + const client = new STSClient({}); + this.region = await client.config.region(); + + if (!this.region) { + throw new Error( + 'CustomAwsSupplier: Unable to resolve AWS region. Please set the AWS_REGION environment variable or configure it in your ~/.aws/config file.', + ); + } + + return this.region; + } + + /** + * Retrieves AWS security credentials using the AWS SDK's default provider chain. + */ + async getAwsSecurityCredentials(_context) { + // Call the initialized provider. It will return cached creds or refresh if needed. + const awsCredentials = await this.awsCredentialsProvider(); + + // This check is often redundant as the SDK provider throws on failure, + // but serves as an extra safeguard. + if (!awsCredentials.accessKeyId || !awsCredentials.secretAccessKey) { + throw new Error( + 'Unable to resolve AWS credentials from the node provider chain. ' + + 'Ensure your AWS CLI is configured, or AWS environment variables (like AWS_ACCESS_KEY_ID) are set.', + ); + } + + // Map the AWS SDK format to the google-auth-library format. + const awsSecurityCredentials = { + accessKeyId: awsCredentials.accessKeyId, + secretAccessKey: awsCredentials.secretAccessKey, + token: awsCredentials.sessionToken, + }; + + return awsSecurityCredentials; + } +} + +async function main() { + const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE; + const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + const gcsBucketName = process.env.GCS_BUCKET_NAME; + + if (!gcpAudience || !saImpersonationUrl || !gcsBucketName) { + throw new Error( + 'Missing required environment variables. Please check your .env file or environment settings. Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, GCS_BUCKET_NAME', + ); + } + + // 1. Instantiate the custom supplier. + const customSupplier = new CustomAwsSupplier(); + + // 2. Configure the AwsClient options using the constants. + const clientOptions = { + audience: gcpAudience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + service_account_impersonation_url: saImpersonationUrl, + aws_security_credentials_supplier: customSupplier, + }; + + // 3. Create the auth client + const client = new AwsClient(clientOptions); + + // 4. Construct the URL for the Cloud Storage JSON API to get bucket metadata. + + const bucketUrl = `https://storage.googleapis.com/storage/v1/b/${gcsBucketName}`; + console.log(`[Test] Getting metadata for bucket: ${gcsBucketName}...`); + console.log(`[Test] Request URL: ${bucketUrl}`); + + // 5. Use the client to make an authenticated request. + const res = await client.request({url: bucketUrl}); + + console.log('\n--- SUCCESS! ---'); + console.log('Successfully authenticated and retrieved bucket data:'); + console.log(JSON.stringify(res.data, null, 2)); +} + +// Execute the test. +main().catch(error => { + console.error('\n--- FAILED ---'); + const fullError = error.response?.data || error; + console.error(JSON.stringify(fullError, null, 2)); + process.exitCode = 1; +}); diff --git a/samples/customCredentialSupplierOktaWorkload.js b/samples/customCredentialSupplierOktaWorkload.js new file mode 100644 index 00000000..1f2d19db --- /dev/null +++ b/samples/customCredentialSupplierOktaWorkload.js @@ -0,0 +1,181 @@ +// 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. + +'use strict'; + +const {IdentityPoolClient} = require('google-auth-library'); +const {Gaxios} = require('gaxios'); +require('dotenv').config(); + +// Workload Identity Pool Configuration +const gcpWorkloadAudience = process.env.GCP_WORKLOAD_AUDIENCE; +const serviceAccountImpersonationUrl = + process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; +const gcsBucketName = process.env.GCS_BUCKET_NAME; + +// Okta Configuration +const oktaDomain = process.env.OKTA_DOMAIN; // e.g., 'https://dev-12345.okta.com' +const oktaClientId = process.env.OKTA_CLIENT_ID; // The Client ID of your Okta M2M application +const oktaClientSecret = process.env.OKTA_CLIENT_SECRET; // The Client Secret of your Okta M2M application + +// Constants for the authentication flow +const TOKEN_URL = 'https://sts.googleapis.com/v1/token'; +const SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:jwt'; + +/** + * A custom SubjectTokenSupplier that authenticates with Okta using the + * Client Credentials grant flow. + * + * This flow is designed for machine-to-machine (M2M) authentication and + * exchanges the application'''s client_id and client_secret for an access token. + */ +class OktaClientCredentialsSupplier { + constructor(domain, clientId, clientSecret) { + this.oktaTokenUrl = `${domain}/oauth2/default/v1/token`; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.accessToken = null; + this.expiryTime = 0; + this.gaxios = new Gaxios(); + console.log('OktaClientCredentialsSupplier initialized.'); + } + + /** + * Main method called by the auth library. It will fetch a new token if one + * is not already cached. + * @returns {Promise} A promise that resolves with the Okta Access token. + */ + async getSubjectToken() { + // Check if the current token is still valid (with a 60-second buffer). + const isTokenValid = + this.accessToken && Date.now() < this.expiryTime - 60 * 1000; + + if (isTokenValid) { + console.log('[Supplier] Returning cached Okta Access token.'); + return this.accessToken; + } + + console.log( + '[Supplier] Token is missing or expired. Fetching new Okta Access token via Client Credentials grant...', + ); + const {accessToken, expiresIn} = await this.fetchOktaAccessToken(); + this.accessToken = accessToken; + // Calculate the absolute expiry time in milliseconds. + this.expiryTime = Date.now() + expiresIn * 1000; + return this.accessToken; + } + + /** + * Performs the Client Credentials grant flow by making a POST request to Okta'''s token endpoint. + * @returns {Promise<{accessToken: string, expiresIn: number}>} A promise that resolves with the Access Token and expiry from Okta. + */ + async fetchOktaAccessToken() { + const params = new URLSearchParams(); + params.append('grant_type', 'client_credentials'); + + // For Client Credentials, scopes are optional and define the permissions + // the token will have. If you have custom scopes, add them here. + params.append('scope', 'gcp.test.read'); + + // The client_id and client_secret are sent in a Basic Auth header. + const authHeader = + 'Basic ' + + Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); + + try { + const response = await this.gaxios.request({ + url: this.oktaTokenUrl, + method: 'POST', + headers: { + Authorization: authHeader, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: params.toString(), + }); + + const {access_token, expires_in} = response.data; + + if (access_token && expires_in) { + console.log( + `[Supplier] Successfully received Access Token from Okta. Expires in ${expires_in} seconds.`, + ); + return {accessToken: access_token, expiresIn: expires_in}; + } else { + throw new Error( + 'Access token or expires_in not found in Okta response.', + ); + } + } catch (error) { + console.error( + '[Supplier] Error fetching token from Okta:', + error.response?.data || error.message, + ); + throw new Error( + 'Failed to authenticate with Okta using Client Credentials grant.', + ); + } + } +} + +/** + * Main function to demonstrate the custom supplier. + */ +async function main() { + if ( + !gcpWorkloadAudience || + !gcsBucketName || + !oktaDomain || + !oktaClientId || + !oktaClientSecret + ) { + throw new Error( + 'Missing required environment variables. Please check your .env file.', + ); + } + + // 1. Instantiate our custom supplier with Okta credentials. + const oktaSupplier = new OktaClientCredentialsSupplier( + oktaDomain, + oktaClientId, + oktaClientSecret, + ); + + // 2. Instantiate an IdentityPoolClient directly with the required configuration. + // This client is specialized for workload identity federation flows. + const client = new IdentityPoolClient({ + audience: gcpWorkloadAudience, + subject_token_type: SUBJECT_TOKEN_TYPE, + token_url: TOKEN_URL, + subject_token_supplier: oktaSupplier, + service_account_impersonation_url: serviceAccountImpersonationUrl, + }); + + // 3. Construct the URL for the Cloud Storage JSON API to get bucket metadata. + const bucketUrl = `https://storage.googleapis.com/storage/v1/b/${gcsBucketName}`; + console.log(`[Test] Getting metadata for bucket: ${gcsBucketName}...`); + console.log(`[Test] Request URL: ${bucketUrl}`); + + // 4. Use the client to make an authenticated request. + const res = await client.request({url: bucketUrl}); + + console.log('--- SUCCESS! ---'); + console.log('Successfully authenticated and retrieved bucket data:'); + console.log(JSON.stringify(res.data, null, 2)); +} + +main().catch(error => { + console.error('--- FAILED ---'); + const fullError = error.response?.data || error; + console.error(JSON.stringify(fullError, null, 2)); + process.exitCode = 1; +}); diff --git a/samples/package.json b/samples/package.json index bd026b86..53a900e5 100644 --- a/samples/package.json +++ b/samples/package.json @@ -15,11 +15,15 @@ "dependencies": { "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", + "@aws-sdk/credential-providers": "^3.58.0", "@googleapis/iam": "^32.0.0", "google-auth-library": "^10.4.0", + "dotenv": "^16.3.1", + "gaxios": "^7.0.0", "node-fetch": "^2.3.0", "open": "^9.0.0", - "server-destroy": "^1.0.1" + "server-destroy": "^1.0.1", + "@aws-sdk/client-sts": "^3.58.0" }, "devDependencies": { "chai": "^4.2.0", From 2f54532b2183f9cd3f0b587a5724d1840966a697 Mon Sep 17 00:00:00 2001 From: miguel Date: Wed, 1 Oct 2025 08:31:28 -0700 Subject: [PATCH 645/662] fix: disable linkinator until 429 issue is fixed. (#2138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: disable linkinator until 429 issue is fixed. * fix: remove docs from ci * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: broken link in readme * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: remove docs generation in kokoro job * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- README.md | 6 ++++++ package.json | 3 +-- samples/README.md | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eeac66b5..864dcc27 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,8 @@ your workloads are running in other AWS environments, such as ECS, EKS, Fargate, Note that the client does not cache the returned AWS security credentials, so caching logic should be implemented in the supplier to prevent multiple requests for the same resources. +For a sample on how to access Google Cloud resources from AWS with a custom credential supplier, see [samples/customCredentialSupplierAwsWorkload.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierAwsWorkload.js). + ```ts import { AwsClient, AwsSecurityCredentials, AwsSecurityCredentialsSupplier, ExternalAccountSupplierContext } from 'google-auth-library'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; @@ -1103,6 +1105,8 @@ and the workforce pool user project is the project number associated with the [w The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#use_configuration_files_for_sign-in). +For a sample on how to access Google Cloud resources from an Okta identity provider with a custom credential supplier, see [samples/customCredentialSupplierOktaWorkforce.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierOktaWorkforce.js). + ### Using External Identities External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`. @@ -1409,6 +1413,8 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar | Authenticate Implicit With Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateImplicitWithAdc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/authenticateImplicitWithAdc.js,samples/README.md) | | Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) | | Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/credentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/credentials.js,samples/README.md) | +| Custom Credential Supplier Aws Workload | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierAwsWorkload.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/customCredentialSupplierAwsWorkload.js,samples/README.md) | +| Custom Credential Supplier Okta Workload | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierOktaWorkload.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/customCredentialSupplierOktaWorkload.js,samples/README.md) | | Downscopedclient | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/downscopedclient.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/downscopedclient.js,samples/README.md) | | Headers | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/headers.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/headers.js,samples/README.md) | | Id Token From Impersonated Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idTokenFromImpersonatedCredentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idTokenFromImpersonatedCredentials.js,samples/README.md) | diff --git a/package.json b/package.json index c7378870..d1f2ac68 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "karma-sourcemap-loader": "^0.4.0", "karma-webpack": "^5.0.1", "keypair": "^1.0.4", - "linkinator": "^6.1.2", "mocha": "^11.1.0", "mv": "^2.1.1", "ncp": "^2.0.0", @@ -81,7 +80,7 @@ "presystem-test": "npm run compile -- --sourceMap", "webpack": "webpack", "browser-test": "karma start", - "docs-test": "linkinator docs", + "docs-test": "echo 'disabled until linkinator is fixed'", "predocs-test": "npm run docs", "prelint": "cd samples; npm link ../; npm install" }, diff --git a/samples/README.md b/samples/README.md index 5ea1e11e..b0cac43b 100644 --- a/samples/README.md +++ b/samples/README.md @@ -18,6 +18,8 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra * [Authenticate Implicit With Adc](#authenticate-implicit-with-adc) * [Compute](#compute) * [Credentials](#credentials) + * [Custom Credential Supplier Aws Workload](#custom-credential-supplier-aws-workload) + * [Custom Credential Supplier Okta Workload](#custom-credential-supplier-okta-workload) * [Downscopedclient](#downscopedclient) * [Headers](#headers) * [Id Token From Impersonated Credentials](#id-token-from-impersonated-credentials) @@ -153,6 +155,40 @@ __Usage:__ +### Custom Credential Supplier Aws Workload + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierAwsWorkload.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/customCredentialSupplierAwsWorkload.js,samples/README.md) + +__Usage:__ + + +`node samples/customCredentialSupplierAwsWorkload.js` + + +----- + + + + +### Custom Credential Supplier Okta Workload + +View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierOktaWorkload.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/customCredentialSupplierOktaWorkload.js,samples/README.md) + +__Usage:__ + + +`node samples/customCredentialSupplierOktaWorkload.js` + + +----- + + + + ### Downscopedclient View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/downscopedclient.js). From 0852f0a9e5740fad820c8f13c064bb332388344e Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 1 Oct 2025 17:11:46 +0100 Subject: [PATCH 646/662] chore(deps): update dependency jsdoc-fresh to v5 (#2145) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1f2ac68..ae80d64b 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "gts": "^6.0.0", "is-docker": "^3.0.0", "jsdoc": "^4.0.0", - "jsdoc-fresh": "^4.0.0", + "jsdoc-fresh": "^5.0.0", "jsdoc-region-tag": "^3.0.0", "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", From bbee39e07390f4346d87bf70a7c7a3e28bade8f3 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 1 Oct 2025 18:31:42 +0100 Subject: [PATCH 647/662] fix(deps): update dependency @googleapis/iam to v33 (#2146) Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index 53a900e5..eb196045 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@aws-sdk/credential-providers": "^3.58.0", - "@googleapis/iam": "^32.0.0", + "@googleapis/iam": "^33.0.0", "google-auth-library": "^10.4.0", "dotenv": "^16.3.1", "gaxios": "^7.0.0", From 4b9e968422697b40528fc8fdde304f510040af6e Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:03:03 -0400 Subject: [PATCH 648/662] chore: update npmjs.org to npmjs.com (#2147) Source-Link: https://github.com/googleapis/synthtool/commit/c9bbe7c01239553f72752dfa481f4eb84ffd1925 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:d0d37c730ec00f109a1a20d298d6df88a965626f75aaf00c3cce94d56c9e2a9f Co-authored-by: Owl Bot Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/release/publish.cfg | 2 +- README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 50bc3b4e..76bda315 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:84adf917cad8f48c61227febebae7af619882d7c8863d6ab6290a77d45a372cf -# created: 2025-09-10T20:42:34.536728816Z + digest: sha256:d0d37c730ec00f109a1a20d298d6df88a965626f75aaf00c3cce94d56c9e2a9f +# created: 2025-10-01T14:22:55.919514987Z diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index 9ea0570f..080e7d48 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -38,7 +38,7 @@ env_vars: { value: "github/google-auth-library-nodejs/.kokoro/publish.sh" } -# Store the packages we uploaded to npmjs.org and their corresponding +# Store the packages we uploaded to npmjs.com and their corresponding # package-lock.jsons in Placer. That way, we have a record of exactly # what we published, and which version of which tools we used to publish # it, which we can use to generate SBOMs and attestations. diff --git a/README.md b/README.md index 864dcc27..176e200f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # [Google Auth Library: Node.js Client](https://github.com/googleapis/google-auth-library-nodejs) [![release level](https://img.shields.io/badge/release%20level-stable-brightgreen.svg?style=flat)](https://cloud.google.com/terms/launch-stages) -[![npm version](https://img.shields.io/npm/v/google-auth-library.svg)](https://www.npmjs.org/package/google-auth-library) +[![npm version](https://img.shields.io/npm/v/google-auth-library.svg)](https://www.npmjs.com/package/google-auth-library) From 3cdef0139ce4c64a4edca31a967db454a4723464 Mon Sep 17 00:00:00 2001 From: werman Date: Wed, 1 Oct 2025 11:42:22 -0700 Subject: [PATCH 649/662] fix: link to customCredentialSupplierOktaWorkload (#2149) --- .readme-partials.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 0abe2208..a5372fbc 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -1061,7 +1061,7 @@ body: |- The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#use_configuration_files_for_sign-in). - For a sample on how to access Google Cloud resources from an Okta identity provider with a custom credential supplier, see [samples/customCredentialSupplierOktaWorkforce.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierOktaWorkforce.js). + For a sample on how to access Google Cloud resources from an Okta identity provider with a custom credential supplier, see [samples/customCredentialSupplierOktaWorkload.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierOktaWorkload.js). ### Using External Identities From 5b2c7c5d6b55b85434ab6128a7ab257f723e376c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 2 Oct 2025 19:14:46 +0100 Subject: [PATCH 650/662] fix(deps): update dependency dotenv to v17 (#2150) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index eb196045..df188a35 100644 --- a/samples/package.json +++ b/samples/package.json @@ -18,7 +18,7 @@ "@aws-sdk/credential-providers": "^3.58.0", "@googleapis/iam": "^33.0.0", "google-auth-library": "^10.4.0", - "dotenv": "^16.3.1", + "dotenv": "^17.0.0", "gaxios": "^7.0.0", "node-fetch": "^2.3.0", "open": "^9.0.0", From eb2747a6526774f148f11c58f788e8a2bb3bed4d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 3 Oct 2025 17:00:45 +0100 Subject: [PATCH 651/662] chore(deps): update dependency jsdoc-region-tag to v4 (#2152) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae80d64b..58261586 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "is-docker": "^3.0.0", "jsdoc": "^4.0.0", "jsdoc-fresh": "^5.0.0", - "jsdoc-region-tag": "^3.0.0", + "jsdoc-region-tag": "^4.0.0", "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", "karma-coverage": "^2.0.0", From e2c2ffc55fe51d7f6fe68cc8c671e07bd48d3ee0 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 19:38:22 -0400 Subject: [PATCH 652/662] chore: disable renovate for Node github action YAML configs (#2154) chore: disable renovate for github action YAML configs Source-Link: https://github.com/googleapis/synthtool/commit/158d49d854395e4eca4706df556628c418037193 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:bdf89cdfb5b791d382184a7a769862b15c38e94e7d82b268c58d40d8952720f2 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- README.md | 2 +- renovate.json | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 76bda315..0bb507a6 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:d0d37c730ec00f109a1a20d298d6df88a965626f75aaf00c3cce94d56c9e2a9f -# created: 2025-10-01T14:22:55.919514987Z + digest: sha256:bdf89cdfb5b791d382184a7a769862b15c38e94e7d82b268c58d40d8952720f2 +# created: 2025-10-03T19:51:38.870830821Z diff --git a/README.md b/README.md index 176e200f..beb8570e 100644 --- a/README.md +++ b/README.md @@ -1105,7 +1105,7 @@ and the workforce pool user project is the project number associated with the [w The values for audience, service account impersonation URL, and any other builder field can also be found by generating a [credential configuration file with the gcloud CLI](https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#use_configuration_files_for_sign-in). -For a sample on how to access Google Cloud resources from an Okta identity provider with a custom credential supplier, see [samples/customCredentialSupplierOktaWorkforce.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierOktaWorkforce.js). +For a sample on how to access Google Cloud resources from an Okta identity provider with a custom credential supplier, see [samples/customCredentialSupplierOktaWorkload.js](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/customCredentialSupplierOktaWorkload.js). ### Using External Identities diff --git a/renovate.json b/renovate.json index c5c702cf..f39fd323 100644 --- a/renovate.json +++ b/renovate.json @@ -15,6 +15,10 @@ { "extends": "packages:linters", "groupName": "linters" + }, + { + "matchManagers": ["github-actions"], + "enabled": false } ], "ignoreDeps": ["typescript"] From 0006ae223b4197da2ce154777fa853592adc1f2c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 13 Oct 2025 14:47:03 +0100 Subject: [PATCH 653/662] fix(deps): update dependency @googleapis/iam to v34 (#2159) --- samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/package.json b/samples/package.json index df188a35..ae452c11 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "@google-cloud/language": "^7.0.0", "@google-cloud/storage": "^7.0.0", "@aws-sdk/credential-providers": "^3.58.0", - "@googleapis/iam": "^33.0.0", + "@googleapis/iam": "^34.0.0", "google-auth-library": "^10.4.0", "dotenv": "^17.0.0", "gaxios": "^7.0.0", From 7e547d48ba4244fc69e39f79f64d05a3a6f84d9d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 13 Oct 2025 15:28:44 +0100 Subject: [PATCH 654/662] fix(deps): update dependency gcp-metadata to v8 (#2158) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 58261586..64b28912 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", - "gcp-metadata": "^7.0.0", + "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" From fbc6b112f7872cf822133f4cc7822c913f05be2f Mon Sep 17 00:00:00 2001 From: Gautam Sharda <57648023+GautamSharda@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:46:28 -0700 Subject: [PATCH 655/662] =?UTF-8?q?Revert=20"feat:=20add=20console=20warni?= =?UTF-8?q?ngs=20for=20mitigating=20file=20based=20credential=20load=20?= =?UTF-8?q?=E2=80=A6=20(#2143)"=20(#2160)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit cae596bcf3de1376c57c2cf92a45a8aff8ddd593. --- src/auth/externalclient.ts | 3 --- src/auth/googleauth.ts | 16 ---------------- src/auth/impersonated.ts | 3 --- 3 files changed, 22 deletions(-) diff --git a/src/auth/externalclient.ts b/src/auth/externalclient.ts index 8157a62e..f9a7cb0f 100644 --- a/src/auth/externalclient.ts +++ b/src/auth/externalclient.ts @@ -73,9 +73,6 @@ export class ExternalAccountClient { static fromJSON( options: ExternalAccountClientOptions, ): BaseExternalAccountClient | null { - console.warn( - 'The `fromJSON` method does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', - ); if (options && options.type === EXTERNAL_ACCOUNT_TYPE) { if ((options as AwsClientOptions).credential_source?.environment_id) { return new AwsClient(options as AwsClientOptions); diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 234dcdaa..f566a3b8 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -275,19 +275,9 @@ export class GoogleAuth { this._cachedProjectId = opts.projectId || null; this.cachedCredential = opts.authClient || null; this.keyFilename = opts.keyFilename || opts.keyFile; - if (this.keyFilename) { - console.warn( - 'The `keyFilename` option is deprecated. Please use the `credentials` option instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', - ); - } this.scopes = opts.scopes; this.clientOptions = opts.clientOptions || {}; this.jsonContent = opts.credentials || null; - if (this.jsonContent) { - console.warn( - 'The `credentials` option is deprecated. Please use the `auth` object constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', - ); - } this.apiKey = opts.apiKey || this.clientOptions.apiKey || null; // Cannot use both API Key + Credentials @@ -776,9 +766,6 @@ export class GoogleAuth { json: JWTInput | ImpersonatedJWTInput, options: AuthClientOptions = {}, ): JSONClient { - console.warn( - 'The `fromJSON` method is deprecated. Please use the `JWT` constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', - ); let client: JSONClient; // user's preferred universe domain @@ -895,9 +882,6 @@ export class GoogleAuth { optionsOrCallback: AuthClientOptions | CredentialCallback = {}, callback?: CredentialCallback, ): Promise | void { - console.warn( - 'The `fromStream` method is deprecated. Please use the `JWT` constructor with a parsed stream instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', - ); let options: AuthClientOptions = {}; if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index 708b6a0f..97742ef6 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -121,9 +121,6 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { */ constructor(options: ImpersonatedOptions = {}) { super(options); - console.warn( - 'The `Impersonated` constructor does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', - ); // Start with an expired refresh token, which will automatically be // refreshed before the first API call is made. this.credentials = { From 069c42745d93556ead3d4140b9e43a13182356fb Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:02:14 -0400 Subject: [PATCH 656/662] chore: update setup-node in github workflows (#2161) Source-Link: https://github.com/googleapis/synthtool/commit/bfba27452fbd72297a5e87dd61ac2804e850a86f Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:da8a4a745d5eb96f07fa99a550afe49ac80dcd9b93a8d914e4f1f6f1cda653ff Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/issues-no-repro.yaml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 0bb507a6..717a7be2 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:bdf89cdfb5b791d382184a7a769862b15c38e94e7d82b268c58d40d8952720f2 -# created: 2025-10-03T19:51:38.870830821Z + digest: sha256:da8a4a745d5eb96f07fa99a550afe49ac80dcd9b93a8d914e4f1f6f1cda653ff +# created: 2025-10-14T15:58:07.94636513Z diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 10c83fc2..87f50077 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: node: [18, 20, 22, 24] steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - run: node --version @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 18 - run: node --version @@ -44,7 +44,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 18 - run: npm install --engine-strict @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 18 - run: npm install @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 18 - run: npm install diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 53105402..66ceadb9 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,7 +11,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 18 - run: npm install From be41d83846732c0b12f60cfd34c0b8ab09386b8f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:49:37 -0400 Subject: [PATCH 657/662] chore(main): release 10.4.1 (#2148) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c8b8a5e..dd78fa70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.4.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.4.0...v10.4.1) (2025-10-14) + + +### Bug Fixes + +* **deps:** Update dependency @googleapis/iam to v33 ([#2146](https://github.com/googleapis/google-auth-library-nodejs/issues/2146)) ([bbee39e](https://github.com/googleapis/google-auth-library-nodejs/commit/bbee39e07390f4346d87bf70a7c7a3e28bade8f3)) +* **deps:** Update dependency @googleapis/iam to v34 ([#2159](https://github.com/googleapis/google-auth-library-nodejs/issues/2159)) ([0006ae2](https://github.com/googleapis/google-auth-library-nodejs/commit/0006ae223b4197da2ce154777fa853592adc1f2c)) +* **deps:** Update dependency dotenv to v17 ([#2150](https://github.com/googleapis/google-auth-library-nodejs/issues/2150)) ([5b2c7c5](https://github.com/googleapis/google-auth-library-nodejs/commit/5b2c7c5d6b55b85434ab6128a7ab257f723e376c)) +* **deps:** Update dependency gcp-metadata to v8 ([#2158](https://github.com/googleapis/google-auth-library-nodejs/issues/2158)) ([7e547d4](https://github.com/googleapis/google-auth-library-nodejs/commit/7e547d48ba4244fc69e39f79f64d05a3a6f84d9d)) +* Disable linkinator until 429 issue is fixed. ([#2138](https://github.com/googleapis/google-auth-library-nodejs/issues/2138)) ([2f54532](https://github.com/googleapis/google-auth-library-nodejs/commit/2f54532b2183f9cd3f0b587a5724d1840966a697)) +* Link to customCredentialSupplierOktaWorkload ([#2149](https://github.com/googleapis/google-auth-library-nodejs/issues/2149)) ([3cdef01](https://github.com/googleapis/google-auth-library-nodejs/commit/3cdef0139ce4c64a4edca31a967db454a4723464)) + ## [10.4.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.3.1...v10.4.0) (2025-09-30) diff --git a/package.json b/package.json index 64b28912..dc492194 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.4.0", + "version": "10.4.1", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index ae452c11..6db32181 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,7 +17,7 @@ "@google-cloud/storage": "^7.0.0", "@aws-sdk/credential-providers": "^3.58.0", "@googleapis/iam": "^34.0.0", - "google-auth-library": "^10.4.0", + "google-auth-library": "^10.4.1", "dotenv": "^17.0.0", "gaxios": "^7.0.0", "node-fetch": "^2.3.0", From 33fb917f01f59058d20a630079d67e815dbf2eb9 Mon Sep 17 00:00:00 2001 From: Tomoya Iwata Date: Fri, 17 Oct 2025 01:01:15 +0900 Subject: [PATCH 658/662] docs: fix AWS security credentials supplier example (#2162) --- .readme-partials.yaml | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index a5372fbc..d7e1a452 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -390,7 +390,7 @@ body: |- private readonly region: string constructor(region: string) { - this.region = options.region; + this.region = region; } async getAwsRegion(context: ExternalAccountSupplierContext): Promise { @@ -419,7 +419,7 @@ body: |- const clientOptions = { audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type. - aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier. + aws_security_credentials_supplier: new AwsSupplier("AWS_REGION"), // Set the custom supplier. service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken', // Set the service account impersonation url. } diff --git a/README.md b/README.md index beb8570e..fd95b59d 100644 --- a/README.md +++ b/README.md @@ -434,7 +434,7 @@ class AwsSupplier implements AwsSecurityCredentialsSupplier { private readonly region: string constructor(region: string) { - this.region = options.region; + this.region = region; } async getAwsRegion(context: ExternalAccountSupplierContext): Promise { @@ -463,7 +463,7 @@ class AwsSupplier implements AwsSecurityCredentialsSupplier { const clientOptions = { audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience. subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type. - aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier. + aws_security_credentials_supplier: new AwsSupplier("AWS_REGION"), // Set the custom supplier. service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken', // Set the service account impersonation url. } From c1281490b3e10d3801fb2a18b2ec8f84538ef60d Mon Sep 17 00:00:00 2001 From: werman Date: Thu, 23 Oct 2025 12:23:48 -0700 Subject: [PATCH 659/662] fix: export ExternalAccountAuthorizedUserCredential (#2166) --- src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.ts b/src/index.ts index b76585d4..02cf8f18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,11 @@ export { PluggableAuthClientOptions, ExecutableError, } from './auth/pluggable-auth-client'; +export { + EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, + ExternalAccountAuthorizedUserClient, + ExternalAccountAuthorizedUserClientOptions, +} from './auth/externalAccountAuthorizedUserClient'; export {PassThroughClient} from './auth/passthrough'; type ALL_EXPORTS = (typeof import('./'))[keyof typeof import('./')]; From 7846bf6fd44db6b3ac4351d4a18eef1809a509e5 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:03:11 -0700 Subject: [PATCH 660/662] chore(main): release 10.4.2 (#2167) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd78fa70..32cd89ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.4.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.4.1...v10.4.2) (2025-10-23) + + +### Bug Fixes + +* Export ExternalAccountAuthorizedUserCredential ([#2166](https://github.com/googleapis/google-auth-library-nodejs/issues/2166)) ([c128149](https://github.com/googleapis/google-auth-library-nodejs/commit/c1281490b3e10d3801fb2a18b2ec8f84538ef60d)) + ## [10.4.1](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.4.0...v10.4.1) (2025-10-14) diff --git a/package.json b/package.json index dc492194..81b00c77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.4.1", + "version": "10.4.2", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 6db32181..5df334bb 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,7 +17,7 @@ "@google-cloud/storage": "^7.0.0", "@aws-sdk/credential-providers": "^3.58.0", "@googleapis/iam": "^34.0.0", - "google-auth-library": "^10.4.1", + "google-auth-library": "^10.4.2", "dotenv": "^17.0.0", "gaxios": "^7.0.0", "node-fetch": "^2.3.0", From f50cb67a284076cd7a4b466cd6c6d32523635ea3 Mon Sep 17 00:00:00 2001 From: sai-sunder-s <4540365+sai-sunder-s@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:33:32 -0700 Subject: [PATCH 661/662] feat: Support scopes from impersonated JSON (#2170) --- src/auth/credentials.ts | 1 + src/auth/googleauth.ts | 3 +- test/fixtures/impersonated-with-scopes.json | 13 +++ test/test.googleauth.ts | 90 +++++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/impersonated-with-scopes.json diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index f0d3876f..3f5f55ab 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -84,6 +84,7 @@ export interface ImpersonatedJWTInput { source_credentials?: JWTInput; service_account_impersonation_url?: string; delegates?: string[]; + scopes?: string[]; } export interface CredentialBody { diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index f566a3b8..4d9832c6 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -710,7 +710,8 @@ export class GoogleAuth { ); } - const targetScopes = this.getAnyScopes() ?? []; + const targetScopes = + (this.scopes || json.scopes || this.defaultScopes) ?? []; return new Impersonated({ ...json, diff --git a/test/fixtures/impersonated-with-scopes.json b/test/fixtures/impersonated-with-scopes.json new file mode 100644 index 00000000..0e50c16a --- /dev/null +++ b/test/fixtures/impersonated-with-scopes.json @@ -0,0 +1,13 @@ +{ + "type": "impersonated_service_account", + "source_credentials": { + "client_id": "oauth_client_id", + "client_secret": "oauth_client_secret", + "refresh_token": "user_refresh_token", + "type": "authorized_user" + }, + "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-email@project-name.iam.gserviceaccount.com:generateAccessToken", + "scopes": [ + "https://www.googleapis.com/auth/drive" + ] +} diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index f61696b4..42e023c9 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -2693,6 +2693,96 @@ describe('googleauth', () => { }); }); + describe('for impersonated_account types', () => { + const userScopes = ['https://www.googleapis.com/auth/user.scope']; + const defaultScopes = ['https://www.googleapis.com/auth/default.scope']; + const jsonScopes = ['https://www.googleapis.com/auth/drive']; + + function mockGenerateAccessToken( + expectedScopes: string[], + serviceAccountEmail = 'service-account-email@project-name.iam.gserviceaccount.com', + ) { + nock('https://oauth2.googleapis.com').post('/token').reply(200, { + access_token: 'source-token', + }); + const scope = nock('https://iamcredentials.googleapis.com') + .post( + `/v1/projects/-/serviceAccounts/${serviceAccountEmail}:generateAccessToken`, + (body: {scope: string[]}) => { + assert.deepStrictEqual(body.scope, expectedScopes); + return true; + }, + ) + .reply(200, { + accessToken: 'impersonated-token', + expireTime: new Date(Date.now() + 3600 * 1000).toISOString(), + }); + return scope; + } + + it('should load scopes from the JSON file', async () => { + const scope = mockGenerateAccessToken(jsonScopes); + const auth = new GoogleAuth({ + keyFilename: './test/fixtures/impersonated-with-scopes.json', + }); + const client = (await auth.getClient()) as Impersonated; + await client.getRequestHeaders(); + scope.done(); + }); + + it('should prefer user scopes over JSON scopes', async () => { + const scope = mockGenerateAccessToken(userScopes); + const auth = new GoogleAuth({ + keyFilename: './test/fixtures/impersonated-with-scopes.json', + scopes: userScopes, + }); + const client = (await auth.getClient()) as Impersonated; + await client.getRequestHeaders(); + scope.done(); + }); + + it('should prefer JSON scopes over default scopes', async () => { + const scope = mockGenerateAccessToken(jsonScopes); + const auth = new GoogleAuth({ + keyFilename: './test/fixtures/impersonated-with-scopes.json', + }); + auth.defaultScopes = defaultScopes; + const client = (await auth.getClient()) as Impersonated; + await client.getRequestHeaders(); + scope.done(); + }); + + it('should use user scopes when JSON has no scopes', async () => { + const scope = mockGenerateAccessToken( + userScopes, + 'target@project.iam.gserviceaccount.com', + ); + const auth = new GoogleAuth({ + keyFilename: + './test/fixtures/impersonated_application_default_credentials.json', + scopes: userScopes, + }); + const client = (await auth.getClient()) as Impersonated; + await client.getRequestHeaders(); + scope.done(); + }); + + it('should fall back to default scopes when no other scopes are present', async () => { + const scope = mockGenerateAccessToken( + defaultScopes, + 'target@project.iam.gserviceaccount.com', + ); + const auth = new GoogleAuth({ + keyFilename: + './test/fixtures/impersonated_application_default_credentials.json', + }); + auth.defaultScopes = defaultScopes; + const client = (await auth.getClient()) as Impersonated; + await client.getRequestHeaders(); + scope.done(); + }); + }); + describe('for external_account_authorized_user types', () => { /** * @return A copy of the external account authorized user JSON auth object From c0ea9cd86a4e10a5efcfe93a7cab66f6dc3916af Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:39:16 -0700 Subject: [PATCH 662/662] chore(main): release 10.5.0 (#2171) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32cd89ea..8665789c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/google-auth-library?activeTab=versions +## [10.5.0](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.4.2...v10.5.0) (2025-10-30) + + +### Features + +* Support scopes from impersonated JSON ([#2170](https://github.com/googleapis/google-auth-library-nodejs/issues/2170)) ([f50cb67](https://github.com/googleapis/google-auth-library-nodejs/commit/f50cb67a284076cd7a4b466cd6c6d32523635ea3)) + ## [10.4.2](https://github.com/googleapis/google-auth-library-nodejs/compare/v10.4.1...v10.4.2) (2025-10-23) diff --git a/package.json b/package.json index 81b00c77..60262bce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-auth-library", - "version": "10.4.2", + "version": "10.5.0", "author": "Google Inc.", "description": "Google APIs Authentication Client Library for Node.js", "engines": { diff --git a/samples/package.json b/samples/package.json index 5df334bb..7a6b34ef 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,7 +17,7 @@ "@google-cloud/storage": "^7.0.0", "@aws-sdk/credential-providers": "^3.58.0", "@googleapis/iam": "^34.0.0", - "google-auth-library": "^10.4.2", + "google-auth-library": "^10.5.0", "dotenv": "^17.0.0", "gaxios": "^7.0.0", "node-fetch": "^2.3.0",