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'); +});