Skip to content

Commit 4a14e8c

Browse files
BigTailWolfaeitzmanlsiracdanielbankheadd-goog
authored
feat: Adding support of client authentication method. (#1814)
* feat: support extra parameter of client authentication method * fix lint * fix lint * fix lint * fix lint * Update src/auth/oauth2client.ts Co-authored-by: aeitzman <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: aeitzman <[email protected]> * fix the 'any' type into strict check * fix lint * fix lint * Update src/auth/oauth2client.ts Co-authored-by: Leo <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Leo <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Leo <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Leo <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Leo <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: aeitzman <[email protected]> * address comments * fix tests * fix tests * fix tests and lint * Update src/auth/oauth2client.ts Co-authored-by: Leo <[email protected]> * addressing comments * adding validation of no auth header * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update test/test.oauth2.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update test/test.oauth2.ts Co-authored-by: Daniel Bankhead <[email protected]> * Update test/test.oauth2.ts Co-authored-by: Daniel Bankhead <[email protected]> * fix CI after apply changes * Update src/auth/oauth2client.ts Co-authored-by: Daniel Bankhead <[email protected]> * fix syntax of post value * fix lint * fix the client_secret field * refactor: interface and readability --------- Co-authored-by: aeitzman <[email protected]> Co-authored-by: Leo <[email protected]> Co-authored-by: Daniel Bankhead <[email protected]> Co-authored-by: Daniel Bankhead <[email protected]>
1 parent 0202365 commit 4a14e8c

File tree

3 files changed

+142
-7
lines changed

3 files changed

+142
-7
lines changed

src/auth/oauth2client.ts

+48-6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ export enum CertificateFormat {
6868
JWK = 'JWK',
6969
}
7070

71+
/**
72+
* The client authentication type. Supported values are basic, post, and none.
73+
* https://datatracker.ietf.org/doc/html/rfc7591#section-2
74+
*/
75+
export enum ClientAuthentication {
76+
ClientSecretPost = 'ClientSecretPost',
77+
ClientSecretBasic = 'ClientSecretBasic',
78+
None = 'None',
79+
}
80+
7181
export interface GetTokenOptions {
7282
code: string;
7383
codeVerifier?: string;
@@ -86,6 +96,19 @@ export interface GetTokenOptions {
8696
redirect_uri?: string;
8797
}
8898

99+
/**
100+
* An interface for preparing {@link GetTokenOptions} as a querystring.
101+
*/
102+
interface GetTokenQuery {
103+
client_id?: string;
104+
client_secret?: string;
105+
code_verifier?: string;
106+
code: string;
107+
grant_type: 'authorization_code';
108+
redirect_uri?: string;
109+
[key: string]: string | undefined;
110+
}
111+
89112
export interface TokenInfo {
90113
/**
91114
* The application that is the intended user of the access token.
@@ -475,6 +498,12 @@ export interface OAuth2ClientOptions extends AuthClientOptions {
475498
* The allowed OAuth2 token issuers.
476499
*/
477500
issuers?: string[];
501+
/**
502+
* The client authentication type. Supported values are basic, post, and none.
503+
* Defaults to post if not provided.
504+
* https://datatracker.ietf.org/doc/html/rfc7591#section-2
505+
*/
506+
clientAuthentication?: ClientAuthentication;
478507
}
479508

480509
// Re-exporting here for backwards compatibility
@@ -491,6 +520,7 @@ export class OAuth2Client extends AuthClient {
491520
protected refreshTokenPromises = new Map<string, Promise<GetTokenResponse>>();
492521
readonly endpoints: Readonly<OAuth2ClientEndpoints>;
493522
readonly issuers: string[];
523+
readonly clientAuthentication: ClientAuthentication;
494524

495525
// TODO: refactor tests to make this private
496526
_clientId?: string;
@@ -542,6 +572,8 @@ export class OAuth2Client extends AuthClient {
542572
oauth2IapPublicKeyUrl: 'https://www.gstatic.com/iap/verify/public_key',
543573
...opts.endpoints,
544574
};
575+
this.clientAuthentication =
576+
opts.clientAuthentication || ClientAuthentication.ClientSecretPost;
545577

546578
this.issuers = opts.issuers || [
547579
'accounts.google.com',
@@ -660,20 +692,30 @@ export class OAuth2Client extends AuthClient {
660692
options: GetTokenOptions
661693
): Promise<GetTokenResponse> {
662694
const url = this.endpoints.oauth2TokenUrl.toString();
663-
const values = {
664-
code: options.code,
695+
const headers: Headers = {
696+
'Content-Type': 'application/x-www-form-urlencoded',
697+
};
698+
const values: GetTokenQuery = {
665699
client_id: options.client_id || this._clientId,
666-
client_secret: this._clientSecret,
667-
redirect_uri: options.redirect_uri || this.redirectUri,
668-
grant_type: 'authorization_code',
669700
code_verifier: options.codeVerifier,
701+
code: options.code,
702+
grant_type: 'authorization_code',
703+
redirect_uri: options.redirect_uri || this.redirectUri,
670704
};
705+
if (this.clientAuthentication === ClientAuthentication.ClientSecretBasic) {
706+
const basic = Buffer.from(`${this._clientId}:${this._clientSecret}`);
707+
708+
headers['Authorization'] = `Basic ${basic.toString('base64')}`;
709+
}
710+
if (this.clientAuthentication === ClientAuthentication.ClientSecretPost) {
711+
values.client_secret = this._clientSecret;
712+
}
671713
const res = await this.transporter.request<CredentialRequest>({
672714
...OAuth2Client.RETRY_CONFIG,
673715
method: 'POST',
674716
url,
675717
data: querystring.stringify(values),
676-
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
718+
headers,
677719
});
678720
const tokens = res.data as Credentials;
679721
if (res.data && res.data.expires_in) {

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export {
4444
RefreshOptions,
4545
TokenInfo,
4646
VerifyIdTokenOptions,
47+
ClientAuthentication,
4748
} from './auth/oauth2client';
4849
export {LoginTicket, TokenPayload} from './auth/loginticket';
4950
export {

test/test.oauth2.ts

+93-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ import * as path from 'path';
2323
import * as qs from 'querystring';
2424
import * as sinon from 'sinon';
2525

26-
import {CodeChallengeMethod, Credentials, OAuth2Client} from '../src';
26+
import {
27+
CodeChallengeMethod,
28+
Credentials,
29+
OAuth2Client,
30+
ClientAuthentication,
31+
} from '../src';
2732
import {LoginTicket} from '../src/auth/loginticket';
2833

2934
nock.disableNetConnect();
@@ -1366,6 +1371,7 @@ describe('oauth2', () => {
13661371
reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'},
13671372
})
13681373
.post('/token')
1374+
.matchHeader('authorization', value => value === undefined)
13691375
.reply(200, {
13701376
access_token: 'abc',
13711377
refresh_token: '123',
@@ -1421,6 +1427,92 @@ describe('oauth2', () => {
14211427
assert.strictEqual(params.client_id, 'overridden');
14221428
});
14231429

1430+
it('getToken should use basic header auth if provided in options', async () => {
1431+
const authurl = 'https://sts.googleapis.com/v1/';
1432+
const basic_auth =
1433+
'Basic ' +
1434+
Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
1435+
const scope = nock(authurl)
1436+
.post('/oauthtoken')
1437+
.matchHeader('Authorization', basic_auth)
1438+
.reply(200, {
1439+
access_token: 'abc',
1440+
refresh_token: '123',
1441+
expires_in: 10,
1442+
});
1443+
const opts = {
1444+
clientId: CLIENT_ID,
1445+
clientSecret: CLIENT_SECRET,
1446+
redirectUri: REDIRECT_URI,
1447+
endpoints: {
1448+
oauth2AuthBaseUrl: 'https://auth.cloud.google/authorize',
1449+
oauth2TokenUrl: 'https://sts.googleapis.com/v1/oauthtoken',
1450+
tokenInfoUrl: 'https://sts.googleapis.com/v1/introspect',
1451+
},
1452+
clientAuthentication: ClientAuthentication.ClientSecretBasic,
1453+
};
1454+
const oauth2client = new OAuth2Client(opts);
1455+
const res = await oauth2client.getToken({
1456+
code: 'code here',
1457+
client_id: CLIENT_ID,
1458+
});
1459+
scope.done();
1460+
assert(res.res);
1461+
assert.equal(res.res.data.access_token, 'abc');
1462+
});
1463+
1464+
it('getToken should not use basic header auth if provided none in options and fail', async () => {
1465+
const authurl = 'https://some.example.auth/';
1466+
const scope = nock(authurl)
1467+
.post('/token')
1468+
.matchHeader('Authorization', val => val === undefined)
1469+
.reply(401);
1470+
const opts = {
1471+
clientId: CLIENT_ID,
1472+
clientSecret: CLIENT_SECRET,
1473+
redirectUri: REDIRECT_URI,
1474+
endpoints: {
1475+
oauth2AuthBaseUrl: 'https://auth.cloud.google/authorize',
1476+
oauth2TokenUrl: 'https://some.example.auth/token',
1477+
},
1478+
clientAuthentication: ClientAuthentication.None,
1479+
};
1480+
const oauth2client = new OAuth2Client(opts);
1481+
assert.equal(
1482+
oauth2client.clientAuthentication,
1483+
ClientAuthentication.None
1484+
);
1485+
1486+
try {
1487+
await oauth2client.getToken({
1488+
code: 'code here',
1489+
client_id: CLIENT_ID,
1490+
});
1491+
throw new Error('Expected an error');
1492+
} catch (err) {
1493+
assert(err instanceof GaxiosError);
1494+
assert.equal(err.response?.status, 401);
1495+
} finally {
1496+
scope.done();
1497+
}
1498+
});
1499+
1500+
it('getToken should use auth secret post if not provided in options', async () => {
1501+
const opts = {
1502+
clientId: CLIENT_ID,
1503+
clientSecret: CLIENT_SECRET,
1504+
redirectUri: REDIRECT_URI,
1505+
endpoints: {
1506+
oauth2TokenUrl: 'mytokenurl',
1507+
},
1508+
};
1509+
const oauth2client = new OAuth2Client(opts);
1510+
assert.equal(
1511+
oauth2client.clientAuthentication,
1512+
ClientAuthentication.ClientSecretPost
1513+
);
1514+
});
1515+
14241516
it('should return expiry_date', done => {
14251517
const now = new Date().getTime();
14261518
const scope = nock(baseUrl, {

0 commit comments

Comments
 (0)