Skip to content

Commit ae8bc54

Browse files
authored
feat: Implement UserRefreshClient#fetchIdToken (#1811)
* feat: Implement `UserRefreshClient#fetchIdToken` * test: UserRefreshClient client for getIdTokenClient * refactor: Use `target_audience` > `audience` * refactor: Use `transporter` Removes redundant calls * test: Improve tests
1 parent bb306ef commit ae8bc54

File tree

2 files changed

+81
-16
lines changed

2 files changed

+81
-16
lines changed

src/auth/refreshclient.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
// limitations under the License.
1414

1515
import * as stream from 'stream';
16-
import {JWTInput} from './credentials';
16+
import {CredentialRequest, JWTInput} from './credentials';
1717
import {
1818
GetTokenResponse,
1919
OAuth2Client,
2020
OAuth2ClientOptions,
2121
} from './oauth2client';
22+
import {stringify} from 'querystring';
2223

2324
export const USER_REFRESH_ACCOUNT_TYPE = 'authorized_user';
2425

@@ -78,6 +79,26 @@ export class UserRefreshClient extends OAuth2Client {
7879
return super.refreshTokenNoCache(this._refreshToken);
7980
}
8081

82+
async fetchIdToken(targetAudience: string): Promise<string> {
83+
const res = await this.transporter.request<CredentialRequest>({
84+
...UserRefreshClient.RETRY_CONFIG,
85+
url: this.endpoints.oauth2TokenUrl,
86+
headers: {
87+
'Content-Type': 'application/x-www-form-urlencoded',
88+
},
89+
method: 'POST',
90+
data: stringify({
91+
client_id: this._clientId,
92+
client_secret: this._clientSecret,
93+
grant_type: 'refresh_token',
94+
refresh_token: this._refreshToken,
95+
target_audience: targetAudience,
96+
}),
97+
});
98+
99+
return res.data.id_token!;
100+
}
101+
81102
/**
82103
* Create a UserRefreshClient credentials instance using the given input
83104
* options.

test/test.googleauth.ts

+59-15
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
import {BaseExternalAccountClient} from '../src/auth/baseexternalclient';
5656
import {AuthClient, DEFAULT_UNIVERSE} from '../src/auth/authclient';
5757
import {ExternalAccountAuthorizedUserClient} from '../src/auth/externalAccountAuthorizedUserClient';
58+
import {stringify} from 'querystring';
5859

5960
nock.disableNetConnect();
6061

@@ -1520,37 +1521,80 @@ describe('googleauth', () => {
15201521
assert(client.idTokenProvider instanceof JWT);
15211522
});
15221523

1523-
it('should call getClient for getIdTokenClient', async () => {
1524+
it('should return a UserRefreshClient client for getIdTokenClient', async () => {
15241525
// Set up a mock to return path to a valid credentials file.
15251526
mockEnvVar(
15261527
'GOOGLE_APPLICATION_CREDENTIALS',
1527-
'./test/fixtures/private.json'
1528+
'./test/fixtures/refresh.json'
15281529
);
1530+
mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id');
15291531

1530-
const spy = sinon.spy(auth, 'getClient');
15311532
const client = await auth.getIdTokenClient('a-target-audience');
15321533
assert(client instanceof IdTokenClient);
1533-
assert(spy.calledOnce);
1534+
assert(client.idTokenProvider instanceof UserRefreshClient);
15341535
});
15351536

1536-
it('should fail when using UserRefreshClient', async () => {
1537+
it('should properly use `UserRefreshClient` client for `getIdTokenClient`', async () => {
15371538
// Set up a mock to return path to a valid credentials file.
15381539
mockEnvVar(
15391540
'GOOGLE_APPLICATION_CREDENTIALS',
15401541
'./test/fixtures/refresh.json'
15411542
);
15421543
mockEnvVar('GOOGLE_CLOUD_PROJECT', 'some-project-id');
15431544

1544-
try {
1545-
await auth.getIdTokenClient('a-target-audience');
1546-
} catch (e) {
1547-
assert(e instanceof Error);
1548-
assert(
1549-
e.message.startsWith('Cannot fetch ID token in this environment')
1550-
);
1551-
return;
1552-
}
1553-
assert.fail('failed to throw');
1545+
// Assert `UserRefreshClient`
1546+
const baseClient = await auth.getClient();
1547+
assert(baseClient instanceof UserRefreshClient);
1548+
1549+
// Setup variables
1550+
const idTokenPayload = Buffer.from(JSON.stringify({exp: 100})).toString(
1551+
'base64'
1552+
);
1553+
const testIdToken = `TEST.${idTokenPayload}.TOKEN`;
1554+
const targetAudience = 'a-target-audience';
1555+
const tokenEndpoint = new URL(baseClient.endpoints.oauth2TokenUrl);
1556+
const expectedTokenRequestBody = stringify({
1557+
client_id: baseClient._clientId,
1558+
client_secret: baseClient._clientSecret,
1559+
grant_type: 'refresh_token',
1560+
refresh_token: baseClient._refreshToken,
1561+
target_audience: targetAudience,
1562+
});
1563+
const url = new URL('https://my-protected-endpoint.a.app');
1564+
const expectedRes = {hello: true};
1565+
1566+
// Setup mock endpoints
1567+
nock(tokenEndpoint.origin)
1568+
.post(tokenEndpoint.pathname, expectedTokenRequestBody)
1569+
.reply(200, {id_token: testIdToken});
1570+
nock(url.origin, {
1571+
reqheaders: {
1572+
authorization: `Bearer ${testIdToken}`,
1573+
},
1574+
})
1575+
.get(url.pathname)
1576+
.reply(200, expectedRes);
1577+
1578+
// Make assertions
1579+
const client = await auth.getIdTokenClient(targetAudience);
1580+
assert(client instanceof IdTokenClient);
1581+
assert(client.idTokenProvider instanceof UserRefreshClient);
1582+
1583+
const res = await client.request({url});
1584+
assert.deepStrictEqual(res.data, expectedRes);
1585+
});
1586+
1587+
it('should call getClient for getIdTokenClient', async () => {
1588+
// Set up a mock to return path to a valid credentials file.
1589+
mockEnvVar(
1590+
'GOOGLE_APPLICATION_CREDENTIALS',
1591+
'./test/fixtures/private.json'
1592+
);
1593+
1594+
const spy = sinon.spy(auth, 'getClient');
1595+
const client = await auth.getIdTokenClient('a-target-audience');
1596+
assert(client instanceof IdTokenClient);
1597+
assert(spy.calledOnce);
15541598
});
15551599

15561600
describe('getUniverseDomain', () => {

0 commit comments

Comments
 (0)