Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

Commit 1811b7f

Browse files
feat: support scopes on compute credentials (#642)
1 parent 3016d52 commit 1811b7f

File tree

5 files changed

+50
-6
lines changed

5 files changed

+50
-6
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"client library"
1818
],
1919
"dependencies": {
20+
"arrify": "^1.0.1",
2021
"base64-js": "^1.3.0",
2122
"fast-text-encoding": "^1.0.0",
2223
"gaxios": "^1.2.1",
@@ -29,6 +30,7 @@
2930
},
3031
"devDependencies": {
3132
"@compodoc/compodoc": "^1.1.7",
33+
"@types/arrify": "^1.0.4",
3234
"@types/base64-js": "^1.2.5",
3335
"@types/chai": "^4.1.7",
3436
"@types/execa": "^0.9.0",

src/auth/computeclient.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17+
import * as arrify from 'arrify';
1718
import {GaxiosError, GaxiosOptions, GaxiosPromise} from 'gaxios';
1819
import * as gcpMetadata from 'gcp-metadata';
20+
1921
import * as messages from '../messages';
22+
2023
import {CredentialRequest, Credentials} from './credentials';
2124
import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client';
2225

@@ -26,10 +29,17 @@ export interface ComputeOptions extends RefreshOptions {
2629
* may have multiple service accounts.
2730
*/
2831
serviceAccountEmail?: string;
32+
/**
33+
* The scopes that will be requested when acquiring service account
34+
* credentials. Only applicable to modern App Engine and Cloud Function
35+
* runtimes as of March 2019.
36+
*/
37+
scopes?: string|string[];
2938
}
3039

3140
export class Compute extends OAuth2Client {
3241
private serviceAccountEmail: string;
42+
scopes: string[];
3343

3444
/**
3545
* Google Compute Engine service account credentials.
@@ -43,6 +53,7 @@ export class Compute extends OAuth2Client {
4353
// refreshed before the first API call is made.
4454
this.credentials = {expiry_date: 1, refresh_token: 'compute-placeholder'};
4555
this.serviceAccountEmail = options.serviceAccountEmail || 'default';
56+
this.scopes = arrify(options.scopes);
4657
}
4758

4859
/**
@@ -68,7 +79,13 @@ export class Compute extends OAuth2Client {
6879
const tokenPath = `service-accounts/${this.serviceAccountEmail}/token`;
6980
let data: CredentialRequest;
7081
try {
71-
data = await gcpMetadata.instance(tokenPath);
82+
data = await gcpMetadata.instance({
83+
property: tokenPath,
84+
params: {
85+
scopes: this.scopes
86+
// TODO: clean up before submit, fix upstream type bug
87+
} as {}
88+
});
7289
} catch (e) {
7390
e.message = `Could not refresh access token: ${e.message}`;
7491
this.wrapError(e);

src/auth/googleauth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {isBrowser} from '../isbrowser';
2727
import * as messages from '../messages';
2828
import {DefaultTransporter, Transporter} from '../transporters';
2929

30-
import {Compute} from './computeclient';
30+
import {Compute, ComputeOptions} from './computeclient';
3131
import {CredentialBody, JWTInput} from './credentials';
3232
import {GCPEnv, getEnv} from './envDetect';
3333
import {JWT, JWTOptions} from './jwtclient';
@@ -219,7 +219,7 @@ export class GoogleAuth {
219219
}
220220
}
221221

222-
private async getApplicationDefaultAsync(options?: RefreshOptions):
222+
private async getApplicationDefaultAsync(options: RefreshOptions = {}):
223223
Promise<ADCResponse> {
224224
// If we've already got a cached credential, just return it.
225225
if (this.cachedCredential) {
@@ -276,6 +276,7 @@ export class GoogleAuth {
276276

277277
// For GCE, just return a default ComputeClient. It will take care of
278278
// the rest.
279+
(options as ComputeOptions).scopes = this.scopes;
279280
this.cachedCredential = new Compute(options);
280281
projectId = await this.getProjectId();
281282
return {projectId, credential: this.cachedCredential};

test/test.compute.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@ import * as nock from 'nock';
2020
import * as sinon from 'sinon';
2121
import {Compute} from '../src';
2222
const assertRejects = require('assert-rejects');
23+
import * as qs from 'querystring';
2324

2425
nock.disableNetConnect();
2526

2627
const url = 'http://example.com';
27-
2828
const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`;
29-
function mockToken(statusCode = 200) {
29+
function mockToken(statusCode = 200, scopes?: string[]) {
30+
let path = tokenPath;
31+
if (scopes && scopes.length > 0) {
32+
path += '?' + qs.stringify({scopes});
33+
}
3034
return nock(HOST_ADDRESS)
31-
.get(tokenPath, undefined, {reqheaders: HEADERS})
35+
.get(path, undefined, {reqheaders: HEADERS})
3236
.reply(statusCode, {access_token: 'abc123', expires_in: 10000}, HEADERS);
3337
}
3438

@@ -62,6 +66,17 @@ it('should get an access token for the first request', async () => {
6266
assert.strictEqual(compute.credentials.access_token, 'abc123');
6367
});
6468

69+
it('should include scopes when asking for the token', async () => {
70+
const scopes = [
71+
'https://www.googleapis.com/reader', 'https://www.googleapis.com/auth/plus'
72+
];
73+
const nockScopes = [mockToken(200, scopes), mockExample()];
74+
const compute = new Compute({scopes});
75+
await compute.request({url});
76+
nockScopes.forEach(s => s.done());
77+
assert.strictEqual(compute.credentials.access_token, 'abc123');
78+
});
79+
6580
it('should refresh if access token has expired', async () => {
6681
const scopes = [mockToken(), mockExample()];
6782
compute.credentials.access_token = 'initial-access-token';

test/test.googleauth.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const assertRejects = require('assert-rejects');
2929
import {GoogleAuth, JWT, UserRefreshClient} from '../src';
3030
import {CredentialBody} from '../src/auth/credentials';
3131
import * as envDetect from '../src/auth/envDetect';
32+
import {Compute} from '../src/auth/computeclient';
3233
import * as messages from '../src/messages';
3334

3435
nock.disableNetConnect();
@@ -1136,6 +1137,14 @@ describe('googleauth', () => {
11361137
assert.strictEqual(client.scopes, scopes);
11371138
});
11381139

1140+
it('should allow passing a scope to get a Compute client', async () => {
1141+
const scopes = ['http://examples.com/is/a/scope'];
1142+
const nockScopes = [nockIsGCE(), createGetProjectIdNock()];
1143+
const client = await auth.getClient({scopes}) as Compute;
1144+
assert.strictEqual(client.scopes, scopes);
1145+
nockScopes.forEach(x => x.done());
1146+
});
1147+
11391148
it('should get an access token', async () => {
11401149
const {auth, scopes} = mockGCE();
11411150
scopes.push(createGetProjectIdNock());

0 commit comments

Comments
 (0)