Skip to content

Commit f50cb67

Browse files
authored
feat: Support scopes from impersonated JSON (#2170)
1 parent 7846bf6 commit f50cb67

File tree

4 files changed

+106
-1
lines changed

4 files changed

+106
-1
lines changed

src/auth/credentials.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export interface ImpersonatedJWTInput {
8484
source_credentials?: JWTInput;
8585
service_account_impersonation_url?: string;
8686
delegates?: string[];
87+
scopes?: string[];
8788
}
8889

8990
export interface CredentialBody {

src/auth/googleauth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,8 @@ export class GoogleAuth<T extends AuthClient = AuthClient> {
710710
);
711711
}
712712

713-
const targetScopes = this.getAnyScopes() ?? [];
713+
const targetScopes =
714+
(this.scopes || json.scopes || this.defaultScopes) ?? [];
714715

715716
return new Impersonated({
716717
...json,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"type": "impersonated_service_account",
3+
"source_credentials": {
4+
"client_id": "oauth_client_id",
5+
"client_secret": "oauth_client_secret",
6+
"refresh_token": "user_refresh_token",
7+
"type": "authorized_user"
8+
},
9+
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-email@project-name.iam.gserviceaccount.com:generateAccessToken",
10+
"scopes": [
11+
"https://www.googleapis.com/auth/drive"
12+
]
13+
}

test/test.googleauth.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2693,6 +2693,96 @@ describe('googleauth', () => {
26932693
});
26942694
});
26952695

2696+
describe('for impersonated_account types', () => {
2697+
const userScopes = ['https://www.googleapis.com/auth/user.scope'];
2698+
const defaultScopes = ['https://www.googleapis.com/auth/default.scope'];
2699+
const jsonScopes = ['https://www.googleapis.com/auth/drive'];
2700+
2701+
function mockGenerateAccessToken(
2702+
expectedScopes: string[],
2703+
serviceAccountEmail = 'service-account-email@project-name.iam.gserviceaccount.com',
2704+
) {
2705+
nock('https://oauth2.googleapis.com').post('/token').reply(200, {
2706+
access_token: 'source-token',
2707+
});
2708+
const scope = nock('https://iamcredentials.googleapis.com')
2709+
.post(
2710+
`/v1/projects/-/serviceAccounts/${serviceAccountEmail}:generateAccessToken`,
2711+
(body: {scope: string[]}) => {
2712+
assert.deepStrictEqual(body.scope, expectedScopes);
2713+
return true;
2714+
},
2715+
)
2716+
.reply(200, {
2717+
accessToken: 'impersonated-token',
2718+
expireTime: new Date(Date.now() + 3600 * 1000).toISOString(),
2719+
});
2720+
return scope;
2721+
}
2722+
2723+
it('should load scopes from the JSON file', async () => {
2724+
const scope = mockGenerateAccessToken(jsonScopes);
2725+
const auth = new GoogleAuth({
2726+
keyFilename: './test/fixtures/impersonated-with-scopes.json',
2727+
});
2728+
const client = (await auth.getClient()) as Impersonated;
2729+
await client.getRequestHeaders();
2730+
scope.done();
2731+
});
2732+
2733+
it('should prefer user scopes over JSON scopes', async () => {
2734+
const scope = mockGenerateAccessToken(userScopes);
2735+
const auth = new GoogleAuth({
2736+
keyFilename: './test/fixtures/impersonated-with-scopes.json',
2737+
scopes: userScopes,
2738+
});
2739+
const client = (await auth.getClient()) as Impersonated;
2740+
await client.getRequestHeaders();
2741+
scope.done();
2742+
});
2743+
2744+
it('should prefer JSON scopes over default scopes', async () => {
2745+
const scope = mockGenerateAccessToken(jsonScopes);
2746+
const auth = new GoogleAuth({
2747+
keyFilename: './test/fixtures/impersonated-with-scopes.json',
2748+
});
2749+
auth.defaultScopes = defaultScopes;
2750+
const client = (await auth.getClient()) as Impersonated;
2751+
await client.getRequestHeaders();
2752+
scope.done();
2753+
});
2754+
2755+
it('should use user scopes when JSON has no scopes', async () => {
2756+
const scope = mockGenerateAccessToken(
2757+
userScopes,
2758+
'target@project.iam.gserviceaccount.com',
2759+
);
2760+
const auth = new GoogleAuth({
2761+
keyFilename:
2762+
'./test/fixtures/impersonated_application_default_credentials.json',
2763+
scopes: userScopes,
2764+
});
2765+
const client = (await auth.getClient()) as Impersonated;
2766+
await client.getRequestHeaders();
2767+
scope.done();
2768+
});
2769+
2770+
it('should fall back to default scopes when no other scopes are present', async () => {
2771+
const scope = mockGenerateAccessToken(
2772+
defaultScopes,
2773+
'target@project.iam.gserviceaccount.com',
2774+
);
2775+
const auth = new GoogleAuth({
2776+
keyFilename:
2777+
'./test/fixtures/impersonated_application_default_credentials.json',
2778+
});
2779+
auth.defaultScopes = defaultScopes;
2780+
const client = (await auth.getClient()) as Impersonated;
2781+
await client.getRequestHeaders();
2782+
scope.done();
2783+
});
2784+
});
2785+
26962786
describe('for external_account_authorized_user types', () => {
26972787
/**
26982788
* @return A copy of the external account authorized user JSON auth object

0 commit comments

Comments
 (0)