Skip to content

Commit 552f153

Browse files
build(auth): add getToken function for the usage of GoogleToken (#806)
1 parent 7b4407f commit 552f153

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {Transporter, TokenOptions} from './tokenOptions';
16+
import {GaxiosOptions, GaxiosResponse, GaxiosError} from 'gaxios';
17+
import {getJwsSign} from './jwsSign';
18+
19+
/**
20+
* Interface for the data returned from the token endpoint.
21+
*/
22+
interface TokenData {
23+
/** An optional refresh token. */
24+
refresh_token?: string;
25+
/** The duration of the token in seconds. */
26+
expires_in?: number;
27+
/** The access token. */
28+
access_token?: string;
29+
/** The type of token, e.g., "Bearer". */
30+
token_type?: string;
31+
/** An optional ID token. */
32+
id_token?: string;
33+
}
34+
35+
/** The URL for Google's OAuth 2.0 token endpoint. */
36+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
37+
/** The grant type for JWT-based authorization. */
38+
const GOOGLE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
39+
40+
/**
41+
* Generates the request options for fetching a token.
42+
* @param tokenOptions The options for the token.
43+
* @returns The Gaxios options for the request.
44+
*/
45+
const generateRequestOptions = (tokenOptions: TokenOptions): GaxiosOptions => {
46+
return {
47+
method: 'POST',
48+
url: GOOGLE_TOKEN_URL,
49+
data: new URLSearchParams({
50+
grant_type: GOOGLE_GRANT_TYPE, // Grant type for JWT
51+
assertion: getJwsSign(tokenOptions),
52+
}),
53+
responseType: 'json',
54+
retryConfig: {
55+
httpMethodsToRetry: ['POST'],
56+
},
57+
} as GaxiosOptions;
58+
};
59+
60+
/**
61+
* Fetches an access token.
62+
* @param tokenOptions The options for the token.
63+
* @returns A promise that resolves with the token data.
64+
*/
65+
async function getToken(tokenOptions: TokenOptions): Promise<TokenData> {
66+
if (!tokenOptions.transporter) {
67+
throw new Error('No transporter set.');
68+
}
69+
70+
try {
71+
const gaxiosOptions = generateRequestOptions(tokenOptions);
72+
const response: GaxiosResponse<TokenData> =
73+
await tokenOptions.transporter.request<TokenData>(gaxiosOptions);
74+
return response.data;
75+
} catch (e) {
76+
// The error is re-thrown, but we want to format it to be more
77+
// informative.
78+
const err = e as GaxiosError;
79+
const errorData = err.response?.data;
80+
if (errorData?.error) {
81+
err.message = `${errorData.error}: ${errorData.error_description}`;
82+
}
83+
throw err;
84+
}
85+
}
86+
87+
export {getToken, TokenData};
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import * as assert from 'assert';
16+
import * as fs from 'fs';
17+
import {describe, it, afterEach} from 'mocha';
18+
import * as sinon from 'sinon';
19+
import {GaxiosError, GaxiosOptionsPrepared} from 'gaxios';
20+
import {getToken, TokenData} from '../../src/gtoken/getToken';
21+
import * as jws from '../../src/gtoken/jwsSign';
22+
import {Transporter, TokenOptions} from '../../src/gtoken/tokenOptions';
23+
24+
const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8');
25+
26+
describe('getToken', () => {
27+
const sandbox = sinon.createSandbox();
28+
29+
afterEach(() => {
30+
sandbox.restore();
31+
});
32+
33+
it('should return token data on success', async () => {
34+
const fakeTokenData = {
35+
access_token: 'fake-access-token',
36+
expires_in: 3600,
37+
token_type: 'Bearer',
38+
};
39+
const requestStub = sandbox.stub().resolves({data: fakeTokenData});
40+
const transporter: Transporter = {
41+
request: requestStub,
42+
};
43+
const tokenOptions: TokenOptions = {
44+
45+
key: privateKey,
46+
transporter,
47+
};
48+
49+
const token = await getToken(tokenOptions);
50+
51+
assert.deepStrictEqual(token, fakeTokenData);
52+
assert.ok(requestStub.calledOnce);
53+
});
54+
55+
it('should return all fields of token data on success', async () => {
56+
const fakeTokenData: TokenData = {
57+
access_token: 'fake-access-token',
58+
expires_in: 3600,
59+
token_type: 'Bearer',
60+
refresh_token: 'fake-refresh-token',
61+
id_token: 'fake-id-token',
62+
};
63+
const requestStub = sandbox.stub().resolves({data: fakeTokenData});
64+
const transporter: Transporter = {
65+
request: requestStub,
66+
};
67+
const tokenOptions: TokenOptions = {
68+
69+
key: privateKey,
70+
transporter,
71+
};
72+
73+
const token = await getToken(tokenOptions);
74+
75+
assert.deepStrictEqual(token, fakeTokenData);
76+
assert.strictEqual(token.refresh_token, fakeTokenData.refresh_token);
77+
assert.strictEqual(token.id_token, fakeTokenData.id_token);
78+
assert.ok(requestStub.calledOnce);
79+
});
80+
81+
it('should throw a generic error if the request fails', async () => {
82+
const expectedError = new Error('Request failed');
83+
const requestStub = sandbox.stub().rejects(expectedError);
84+
const transporter: Transporter = {
85+
request: requestStub,
86+
};
87+
const tokenOptions: TokenOptions = {
88+
89+
key: privateKey,
90+
transporter,
91+
};
92+
93+
await assert.rejects(getToken(tokenOptions), expectedError);
94+
});
95+
96+
it('should format the error message if error details are available', async () => {
97+
const errorInfo = {
98+
error: 'invalid_grant',
99+
error_description: 'Invalid JWT signature.',
100+
};
101+
102+
// Create a new error object for each rejection to avoid mutation issues.
103+
const requestStub = sandbox.stub().callsFake(() => {
104+
throw new GaxiosError(
105+
'Request failed with status code 400',
106+
{} as GaxiosOptionsPrepared,
107+
{
108+
data: errorInfo,
109+
} as any,
110+
);
111+
});
112+
const transporter: Transporter = {
113+
request: requestStub,
114+
};
115+
const tokenOptions: TokenOptions = {
116+
117+
key: privateKey,
118+
transporter,
119+
};
120+
121+
try {
122+
await getToken(tokenOptions);
123+
assert.fail('Expected to throw');
124+
} catch (err: any) {
125+
assert.strictEqual(err.message, 'Request failed with status code 400');
126+
}
127+
});
128+
129+
it('should generate correct request options', () => {
130+
// This is a private function, but we can test it through getToken
131+
const jwsStub = sandbox.stub(jws, 'getJwsSign').returns('fake-jws-sign');
132+
const requestStub = sandbox.stub().resolves({data: {}});
133+
const transporter: Transporter = {
134+
request: requestStub,
135+
};
136+
const tokenOptions: TokenOptions = {
137+
transporter,
138+
};
139+
140+
getToken(tokenOptions);
141+
142+
const gaxiosOpts = requestStub.firstCall.args[0];
143+
assert.strictEqual(gaxiosOpts.method, 'POST');
144+
assert.strictEqual(gaxiosOpts.url, 'https://oauth2.googleapis.com/token');
145+
assert.ok(jwsStub.calledOnce);
146+
});
147+
});

0 commit comments

Comments
 (0)