Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use gcp-metadata for compute credentials #409

Merged
merged 2 commits into from
Jul 6, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
26 changes: 7 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,13 +28,12 @@ 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;

// Google Compute Engine metadata server token endpoint.

This comment was marked as spam.



/**
* Google Compute Engine service account credentials.
*
Expand Down Expand Up @@ -67,20 +66,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 +84,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';

This comment was marked as spam.

This comment was marked as spam.

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