Skip to content

Commit 952013a

Browse files
authored
feat: cache id tokens for client headers (#60)
* cache id tokens * lint * lint * resolve comments * Update authMethods.ts
1 parent f35e1f8 commit 952013a

File tree

2 files changed

+71
-21
lines changed

2 files changed

+71
-21
lines changed

packages/toolbox-core/src/toolbox_core/authMethods.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,30 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import {GoogleAuth} from 'google-auth-library';
15+
/*
16+
This module provides functions to obtain Google ID tokens, formatted as "Bearer" tokens,
17+
for use in the "Authorization" header of HTTP requests.
1618
17-
async function getGoogleIdToken(url: string) {
18-
const auth = new GoogleAuth();
19-
const client = await auth.getIdTokenClient(url);
19+
Example User Experience:
20+
import { ToolboxClient } from '@toolbox/core';
21+
import { getGoogleIdToken } from '@toolbox/core/auth'
22+
23+
const URL = 'http://some-url'
24+
const getGoogleIdTokenGetter = () => getGoogleIdToken(URL);
25+
const client = new ToolboxClient(URL, null, {"Authorization": getGoogleIdTokenGetter});
26+
*/
27+
28+
import {GoogleAuth, IdTokenClient} from 'google-auth-library';
29+
30+
const auth = new GoogleAuth();
31+
const clientCache: {[key: string]: IdTokenClient} = {};
32+
33+
export async function getGoogleIdToken(url: string) {
34+
let client = clientCache[url];
35+
if (!client) {
36+
client = await auth.getIdTokenClient(url);
37+
clientCache[url] = client;
38+
}
2039
const id_token = await client.idTokenProvider.fetchIdToken(url);
2140
return `Bearer ${id_token}`;
2241
}
23-
24-
export {getGoogleIdToken};

packages/toolbox-core/test/test.authMethods.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,40 +12,55 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import {getGoogleIdToken} from '../src/toolbox_core/authMethods';
1615
import {GoogleAuth} from 'google-auth-library';
1716

18-
jest.mock('google-auth-library', () => ({GoogleAuth: jest.fn()}));
17+
type GetGoogleIdToken = (url: string) => Promise<string>;
1918

2019
describe('getGoogleIdToken', () => {
2120
const mockUrl = 'https://example.com';
2221
const mockToken = 'mock-id-token';
2322

23+
let getGoogleIdToken: GetGoogleIdToken;
24+
let MockedGoogleAuth: jest.MockedClass<typeof GoogleAuth>;
2425
let mockGetIdTokenClient: jest.Mock;
2526
let mockFetchIdToken: jest.Mock;
2627

2728
beforeEach(() => {
28-
// Reset mocks before each test
29-
mockFetchIdToken = jest.fn().mockResolvedValue(mockToken);
30-
mockGetIdTokenClient = jest.fn().mockResolvedValue({
31-
idTokenProvider: {
32-
fetchIdToken: mockFetchIdToken,
33-
},
34-
});
35-
(GoogleAuth as jest.MockedClass<typeof GoogleAuth>).mockImplementation(
36-
() =>
37-
({
29+
jest.resetModules();
30+
31+
mockFetchIdToken = jest.fn();
32+
mockGetIdTokenClient = jest.fn();
33+
34+
jest.mock('google-auth-library', () => {
35+
return {
36+
GoogleAuth: jest.fn().mockImplementation(() => ({
3837
getIdTokenClient: mockGetIdTokenClient,
39-
}) as unknown as GoogleAuth
40-
);
38+
})),
39+
};
40+
});
41+
42+
// With the mocks fully configured, dynamically require the modules.
43+
// This ensures our code runs against the fresh mocks we just set up.
44+
const authMethods = require('../src/toolbox_core/authMethods');
45+
const {GoogleAuth: GA} = require('google-auth-library');
46+
getGoogleIdToken = authMethods.getGoogleIdToken;
47+
MockedGoogleAuth = GA;
48+
49+
mockGetIdTokenClient.mockResolvedValue({
50+
idTokenProvider: {fetchIdToken: mockFetchIdToken},
51+
});
52+
mockFetchIdToken.mockResolvedValue(mockToken);
4153
});
4254

4355
it('should return a Bearer token on successful fetch', async () => {
4456
const token = await getGoogleIdToken(mockUrl);
4557
expect(token).toBe(`Bearer ${mockToken}`);
46-
expect(GoogleAuth).toHaveBeenCalledTimes(1);
58+
59+
expect(MockedGoogleAuth).toHaveBeenCalledTimes(1);
4760
expect(mockGetIdTokenClient).toHaveBeenCalledWith(mockUrl);
61+
expect(mockGetIdTokenClient).toHaveBeenCalledTimes(1);
4862
expect(mockFetchIdToken).toHaveBeenCalledWith(mockUrl);
63+
expect(mockFetchIdToken).toHaveBeenCalledTimes(1);
4964
});
5065

5166
it('should propagate errors from getIdTokenClient', async () => {
@@ -61,4 +76,22 @@ describe('getGoogleIdToken', () => {
6176

6277
await expect(getGoogleIdToken(mockUrl)).rejects.toThrow(errorMessage);
6378
});
79+
80+
it('should fetch the token only once when called multiple times', async () => {
81+
const token1 = await getGoogleIdToken(mockUrl);
82+
const token2 = await getGoogleIdToken(mockUrl);
83+
await getGoogleIdToken(mockUrl);
84+
85+
expect(token1).toBe(`Bearer ${mockToken}`);
86+
expect(token2).toBe(`Bearer ${mockToken}`);
87+
88+
// `GoogleAuth` constructor was only ever called once.
89+
expect(MockedGoogleAuth).toHaveBeenCalledTimes(1);
90+
91+
// The client is only fetched once.
92+
expect(mockGetIdTokenClient).toHaveBeenCalledTimes(1);
93+
94+
// With our current mock, the token fetching method is called each time.
95+
expect(mockFetchIdToken).toHaveBeenCalledTimes(3);
96+
});
6497
});

0 commit comments

Comments
 (0)