From ae8bc5476f5d93c8516d9a9eb553e7ce7c00edd5 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 9 May 2024 20:26:49 -0700 Subject: [PATCH] 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', () => {