Skip to content

Commit

Permalink
fix: use gcp-metadata for compute credentials (#409)
Browse files Browse the repository at this point in the history
  • Loading branch information
JustinBeckwith authored Jul 6, 2018
1 parent ff8cbe8 commit ba3a57a
Show file tree
Hide file tree
Showing 6 changed files with 26 additions and 120 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"jws": "^3.1.5",
"lodash.isstring": "^4.0.1",
"lru-cache": "^4.1.3",
"retry-axios": "^0.3.2",
"semver": "^5.5.0"
},
"devDependencies": {
Expand Down
23 changes: 4 additions & 19 deletions src/auth/computeclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* limitations under the License.
*/

import axios, {AxiosError, AxiosPromise, AxiosRequestConfig, AxiosResponse} from 'axios';
import {AxiosError, AxiosPromise, AxiosRequestConfig, AxiosResponse} from 'axios';
import * as gcpMetadata from 'gcp-metadata';
import * as rax from 'retry-axios';

import {CredentialRequest, Credentials} from './credentials';
import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client';

Expand All @@ -28,10 +28,6 @@ export interface ComputeOptions extends RefreshOptions {
serviceAccountEmail?: string;
}

// Create a scoped axios instance that will retry 3 times by default
const ax = axios.create();
rax.attach(ax);

export class Compute extends OAuth2Client {
private serviceAccountEmail: string;

Expand Down Expand Up @@ -67,20 +63,10 @@ export class Compute extends OAuth2Client {
*/
protected async refreshTokenNoCache(refreshToken?: string|
null): Promise<GetTokenResponse> {
const url = this.tokenUrl ||
`${gcpMetadata.HOST_ADDRESS}${
gcpMetadata.BASE_PATH}/instance/service-accounts/${
this.serviceAccountEmail}/token`;
const tokenPath = `service-accounts/${this.serviceAccountEmail}/token`;
let res: AxiosResponse<CredentialRequest>;
// request for new token
try {
// TODO: In 2.0, we should remove the ability to configure the tokenUrl,
// and switch this over to use the gcp-metadata package instead.
res = await ax.request<CredentialRequest>({
url,
headers: {[gcpMetadata.HEADER_NAME]: 'Google'},
raxConfig: {noResponseRetries: 3, retry: 3, instance: ax}
} as rax.RaxConfig);
res = await gcpMetadata.instance(tokenPath);
} catch (e) {
e.message = 'Could not refresh access token.';
throw e;
Expand All @@ -95,7 +81,6 @@ export class Compute extends OAuth2Client {
return {tokens, res};
}


protected requestAsync<T>(opts: AxiosRequestConfig, retry = false):
AxiosPromise<T> {
return super.requestAsync<T>(opts, retry).catch(e => {
Expand Down
33 changes: 6 additions & 27 deletions src/auth/oauth2client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,6 @@ export interface GenerateAuthUrlOpts {
code_challenge?: string;
}

export interface AuthClientOpts {
authBaseUrl?: string;
tokenUrl?: string;
}

export interface GetTokenCallback {
(err: AxiosError|null, token?: Credentials|null,
res?: AxiosResponse|null): void;
Expand Down Expand Up @@ -306,8 +301,6 @@ export interface OAuth2ClientOptions extends RefreshOptions {
clientId?: string;
clientSecret?: string;
redirectUri?: string;
authBaseUrl?: string;
tokenUrl?: string;
}

export interface RefreshOptions {
Expand All @@ -322,8 +315,6 @@ export class OAuth2Client extends AuthClient {
private certificateCache: {}|null|undefined = null;
private certificateExpiry: Date|null = null;
protected refreshTokenPromises = new Map<string, Promise<GetTokenResponse>>();
protected authBaseUrl?: string;
protected tokenUrl?: string;

// TODO: refactor tests to make this private
_clientId?: string;
Expand All @@ -348,27 +339,17 @@ export class OAuth2Client extends AuthClient {
* @constructor
*/
constructor(options?: OAuth2ClientOptions);
constructor(
clientId?: string, clientSecret?: string, redirectUri?: string,
opts?: AuthClientOpts);
constructor(clientId?: string, clientSecret?: string, redirectUri?: string);
constructor(
optionsOrClientId?: string|OAuth2ClientOptions, clientSecret?: string,
redirectUri?: string, authClientOpts: AuthClientOpts = {}) {
redirectUri?: string) {
super();
const opts = (optionsOrClientId && typeof optionsOrClientId === 'object') ?
optionsOrClientId :
{
clientId: optionsOrClientId,
clientSecret,
redirectUri,
tokenUrl: authClientOpts.tokenUrl,
authBaseUrl: authClientOpts.authBaseUrl
};
{clientId: optionsOrClientId, clientSecret, redirectUri};
this._clientId = opts.clientId;
this._clientSecret = opts.clientSecret;
this.redirectUri = opts.redirectUri;
this.authBaseUrl = opts.authBaseUrl;
this.tokenUrl = opts.tokenUrl;
this.eagerRefreshThresholdMillis =
opts.eagerRefreshThresholdMillis || 5 * 60 * 1000;
}
Expand Down Expand Up @@ -433,9 +414,7 @@ export class OAuth2Client extends AuthClient {
if (opts.scope instanceof Array) {
opts.scope = opts.scope.join(' ');
}
const rootUrl =
this.authBaseUrl || OAuth2Client.GOOGLE_OAUTH2_AUTH_BASE_URL_;

const rootUrl = OAuth2Client.GOOGLE_OAUTH2_AUTH_BASE_URL_;
return rootUrl + '?' + querystring.stringify(opts);
}

Expand Down Expand Up @@ -488,7 +467,7 @@ export class OAuth2Client extends AuthClient {

private async getTokenAsync(options: GetTokenOptions):
Promise<GetTokenResponse> {
const url = this.tokenUrl || OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_;
const url = OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_;
const values = {
code: options.code,
client_id: options.client_id || this._clientId,
Expand Down Expand Up @@ -544,7 +523,7 @@ export class OAuth2Client extends AuthClient {

protected async refreshTokenNoCache(refreshToken?: string|
null): Promise<GetTokenResponse> {
const url = this.tokenUrl || OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_;
const url = OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_;
const data = {
refresh_token: refreshToken,
client_id: this._clientId,
Expand Down
48 changes: 6 additions & 42 deletions test/test.compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@

import assert from 'assert';
import {AxiosError} from 'axios';
import {BASE_PATH, HOST_ADDRESS} from 'gcp-metadata';
import {BASE_PATH, HEADERS, HOST_ADDRESS} from 'gcp-metadata';
import nock from 'nock';

import {Compute} from '../src';

nock.disableNetConnect();
Expand All @@ -26,10 +27,9 @@ const url = 'http://example.com';

const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`;
function mockToken() {
return nock(HOST_ADDRESS).get(tokenPath).reply(200, {
access_token: 'abc123',
expires_in: 10000
});
return nock(HOST_ADDRESS)
.get(tokenPath, undefined, {reqheaders: HEADERS})
.reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS);
}

function mockExample() {
Expand Down Expand Up @@ -113,42 +113,6 @@ it('should not refresh if access token has not expired', async () => {
scope.done();
});

it('should retry calls to the metadata service if there are network errors',
async () => {
const scopes = [
nock(HOST_ADDRESS)
.get(tokenPath)
.times(2)
.replyWithError({code: 'ENOTFOUND'})
.get(tokenPath)
.reply(200, {access_token: 'abc123', expires_in: 10000}),
mockExample()
];
compute.credentials.access_token = 'initial-access-token';
compute.credentials.expiry_date = (new Date()).getTime() - 10000;
await compute.request({url});
assert.equal(compute.credentials.access_token, 'abc123');
scopes.forEach(s => s.done());
});

it('should retry calls to the metadata service if it returns non-200 errors',
async () => {
const scopes = [
nock(HOST_ADDRESS)
.get(tokenPath)
.times(2)
.reply(500)
.get(tokenPath)
.reply(200, {access_token: 'abc123', expires_in: 10000}),
mockExample()
];
compute.credentials.access_token = 'initial-access-token';
compute.credentials.expiry_date = (new Date()).getTime() - 10000;
await compute.request({url});
assert.equal(compute.credentials.access_token, 'abc123');
scopes.forEach(s => s.done());
});

it('should return false for createScopedRequired', () => {
assert.equal(false, compute.createScopedRequired());
});
Expand Down Expand Up @@ -265,7 +229,7 @@ it('should accept a custom service account', async () => {
nock(HOST_ADDRESS)
.get(`${BASE_PATH}/instance/service-accounts/${
serviceAccountEmail}/token`)
.reply(200, {access_token: 'abc123', expires_in: 10000})
.reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS)
];
await compute.request({url});
scopes.forEach(s => s.done());
Expand Down
26 changes: 10 additions & 16 deletions test/test.googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import assert from 'assert';
import child_process from 'child_process';
import crypto from 'crypto';
import * as fs from 'fs';
import {BASE_PATH, HEADER_NAME, HOST_ADDRESS} from 'gcp-metadata';
import {BASE_PATH, HEADERS, HOST_ADDRESS} from 'gcp-metadata';
import nock from 'nock';
import path from 'path';
import sinon from 'sinon';
Expand Down Expand Up @@ -64,9 +64,7 @@ afterEach(() => {
});

function nockIsGCE() {
return nock(host).get(instancePath).reply(200, {}, {
'metadata-flavor': 'Google'
});
return nock(host).get(instancePath).reply(200, {}, HEADERS);
}

function nockNotGCE() {
Expand All @@ -88,7 +86,7 @@ function nock404GCE() {
function createGetProjectIdNock(projectId: string) {
return nock(host)
.get(`${BASE_PATH}/project/project-id`)
.reply(200, projectId, {'metadata-flavor': 'Google'});
.reply(200, projectId, HEADERS);
}

// Creates a standard JSON auth object for testing.
Expand Down Expand Up @@ -117,10 +115,10 @@ function mockGCE() {
blockGoogleApplicationCredentialEnvironmentVariable();
const auth = new GoogleAuth();
auth._fileExists = () => false;
const scope2 = nock(HOST_ADDRESS).get(tokenPath).reply(200, {
access_token: 'abc123',
expires_in: 10000
});
const scope2 =
nock(HOST_ADDRESS)
.get(tokenPath)
.reply(200, {access_token: 'abc123', expires_in: 10000}, HEADERS);
return {auth, scopes: [scope1, scope2]};
}

Expand Down Expand Up @@ -1139,9 +1137,7 @@ it('getCredentials should get metadata from the server when running on GCE',
}
};
nock.cleanAll();
const scope = nock(host).get(svcAccountPath).reply(200, response, {
'Metadata-Flavor': 'Google'
});
const scope = nock(host).get(svcAccountPath).reply(200, response, HEADERS);
const body = await auth.getCredentials();
assert(body);
assert.equal(
Expand Down Expand Up @@ -1306,7 +1302,7 @@ it('should get the current environment if GKE', async () => {
const {auth, scopes} = mockGCE();
const scope = nock(host)
.get(`${instancePath}/attributes/cluster-name`)
.reply(200, {}, {[HEADER_NAME.toLowerCase()]: 'Google'});
.reply(200, {}, HEADERS);
const env = await auth.getEnv();
assert.equal(env, envDetect.GCPEnv.KUBERNETES_ENGINE);
scope.done();
Expand Down Expand Up @@ -1364,9 +1360,7 @@ it('sign should hit the IAM endpoint if no private_key is available',
nock(iamUri).post(iamPath).reply(200, {signature}),
nock(host)
.get(svcAccountPath)
.reply(
200, {default: {email, private_key: privateKey}},
{'Metadata-Flavor': 'Google'}));
.reply(200, {default: {email, private_key: privateKey}}, HEADERS));
const value = await auth.sign(data);
scopes.forEach(x => x.done());
assert.equal(value, signature);
Expand Down
15 changes: 0 additions & 15 deletions test/test.oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1065,21 +1065,6 @@ it('should return expiry_date', done => {
});
});

it('should accept custom authBaseUrl and tokenUrl', async () => {
const authBaseUrl = 'http://authBaseUrl.com';
const tokenUrl = 'http://tokenUrl.com';
const client = new OAuth2Client(
CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, {authBaseUrl, tokenUrl});
const authUrl = client.generateAuthUrl();
const authUrlParts = url.parse(authUrl);
assert.equal(
authBaseUrl.toLowerCase(),
authUrlParts.protocol + '//' + authUrlParts.hostname);
const scope = nock(tokenUrl).post('/').reply(200, {});
const result = await client.getToken('12345');
scope.done();
});

it('should obtain token info', async () => {
const accessToken = 'abc';
const tokenInfo = {
Expand Down

0 comments on commit ba3a57a

Please sign in to comment.