Skip to content

Commit

Permalink
feat: make x-goog-user-project work for additional auth clients (#848)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe committed Dec 10, 2019
1 parent 7587265 commit 46af865
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 16 deletions.
23 changes: 23 additions & 0 deletions src/auth/authclient.ts
Expand Up @@ -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 = {};

Expand All @@ -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;
}
}
8 changes: 6 additions & 2 deletions src/auth/jwtclient.ts
Expand Up @@ -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.
Expand All @@ -139,7 +143,7 @@ export class JWT extends OAuth2Client {
url,
this.additionalClaims
);
return {headers};
return {headers: this.addSharedMetadataHeaders(headers)};
}
} else {
return super.getRequestMetadataAsync(url);
Expand Down
14 changes: 1 addition & 13 deletions src/auth/oauth2client.ts
Expand Up @@ -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<string, Promise<GetTokenResponse>>();

// TODO: refactor tests to make this private
Expand Down Expand Up @@ -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};
}

/**
Expand Down
12 changes: 12 additions & 0 deletions 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"
}
2 changes: 2 additions & 0 deletions test/test.googleauth.ts
Expand Up @@ -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();
Expand All @@ -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) {
Expand Down
21 changes: 20 additions & 1 deletion test/test.jwt.ts
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
});

0 comments on commit 46af865

Please sign in to comment.