Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adding support of client authentication method. #1814

Merged
merged 42 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
78c2d51
feat: support extra parameter of client authentication method
BigTailWolf May 18, 2024
9fb45d5
fix lint
BigTailWolf May 18, 2024
5c829b3
fix lint
BigTailWolf May 18, 2024
d16618c
fix lint
BigTailWolf May 18, 2024
08c6b67
fix lint
BigTailWolf May 18, 2024
77386a8
Update src/auth/oauth2client.ts
BigTailWolf May 20, 2024
f19d49d
Update src/auth/oauth2client.ts
BigTailWolf May 21, 2024
f1199ba
Merge branch 'main' into b331477791
BigTailWolf May 21, 2024
1d8edaf
fix the 'any' type into strict check
BigTailWolf May 21, 2024
b8d3b8c
fix lint
BigTailWolf May 21, 2024
9ca3d3b
fix lint
BigTailWolf May 21, 2024
19639ab
Update src/auth/oauth2client.ts
BigTailWolf May 23, 2024
9bfdecb
Update src/auth/oauth2client.ts
BigTailWolf May 23, 2024
4af818b
Update src/auth/oauth2client.ts
BigTailWolf May 23, 2024
32f73ed
Update src/auth/oauth2client.ts
BigTailWolf May 23, 2024
c316e9c
Update src/auth/oauth2client.ts
BigTailWolf May 23, 2024
af077ca
Update src/auth/oauth2client.ts
BigTailWolf May 23, 2024
8082f2b
address comments
BigTailWolf May 23, 2024
62ef398
Merge branch 'main' into b331477791
BigTailWolf May 23, 2024
31a97d9
fix tests
BigTailWolf May 23, 2024
50ef233
fix tests
BigTailWolf May 23, 2024
5d4c1d4
fix tests and lint
BigTailWolf May 23, 2024
1e0df4e
Update src/auth/oauth2client.ts
BigTailWolf May 29, 2024
6712b7d
addressing comments
BigTailWolf May 29, 2024
e76b1cd
adding validation of no auth header
BigTailWolf May 29, 2024
a1187ea
Update src/auth/oauth2client.ts
BigTailWolf May 30, 2024
067ede8
Update src/auth/oauth2client.ts
BigTailWolf May 30, 2024
f2c642a
Update src/auth/oauth2client.ts
BigTailWolf May 30, 2024
26e7f5c
Update src/auth/oauth2client.ts
BigTailWolf May 30, 2024
61e127b
Update src/auth/oauth2client.ts
BigTailWolf May 30, 2024
a000b4e
Update src/auth/oauth2client.ts
BigTailWolf May 30, 2024
bb43d01
Update src/auth/oauth2client.ts
BigTailWolf May 30, 2024
4605d4c
Update test/test.oauth2.ts
BigTailWolf May 30, 2024
959dfbf
Update test/test.oauth2.ts
BigTailWolf May 30, 2024
f945b24
Update test/test.oauth2.ts
BigTailWolf May 30, 2024
45f8942
fix CI after apply changes
BigTailWolf May 30, 2024
e142c19
Update src/auth/oauth2client.ts
BigTailWolf May 31, 2024
8ef6417
fix syntax of post value
BigTailWolf May 31, 2024
021b969
fix lint
BigTailWolf May 31, 2024
e5aa5e3
Merge branch 'main' into b331477791
BigTailWolf May 31, 2024
461e648
fix the client_secret field
BigTailWolf May 31, 2024
81a23d3
refactor: interface and readability
danielbankhead May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 48 additions & 6 deletions src/auth/oauth2client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ export enum CertificateFormat {
JWK = 'JWK',
}

/**
* The client authentication type. Supported values are basic, post, and none.
* https://datatracker.ietf.org/doc/html/rfc7591#section-2
*/
export enum ClientAuthentication {
lsirac marked this conversation as resolved.
Show resolved Hide resolved
ClientSecretPost = 'ClientSecretPost',
ClientSecretBasic = 'ClientSecretBasic',
None = 'None',
}

export interface GetTokenOptions {
code: string;
codeVerifier?: string;
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -86,6 +96,19 @@ export interface GetTokenOptions {
redirect_uri?: string;
}

/**
* An interface for preparing {@link GetTokenOptions} as a querystring.
*/
interface GetTokenQuery {
client_id?: string;
client_secret?: string;
code_verifier?: string;
code: string;
grant_type: 'authorization_code';
redirect_uri?: string;
[key: string]: string | undefined;
}

export interface TokenInfo {
/**
* The application that is the intended user of the access token.
Expand Down Expand Up @@ -475,6 +498,12 @@ export interface OAuth2ClientOptions extends AuthClientOptions {
* The allowed OAuth2 token issuers.
*/
issuers?: string[];
/**
* The client authentication type. Supported values are basic, post, and none.
* Defaults to post if not provided.
* https://datatracker.ietf.org/doc/html/rfc7591#section-2
*/
clientAuthentication?: ClientAuthentication;
}

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

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

this.issuers = opts.issuers || [
'accounts.google.com',
Expand Down Expand Up @@ -660,20 +692,30 @@ export class OAuth2Client extends AuthClient {
options: GetTokenOptions
): Promise<GetTokenResponse> {
const url = this.endpoints.oauth2TokenUrl.toString();
const values = {
code: options.code,
const headers: Headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
const values: GetTokenQuery = {
client_id: options.client_id || this._clientId,
client_secret: this._clientSecret,
redirect_uri: options.redirect_uri || this.redirectUri,
grant_type: 'authorization_code',
code_verifier: options.codeVerifier,
code: options.code,
grant_type: 'authorization_code',
redirect_uri: options.redirect_uri || this.redirectUri,
};
if (this.clientAuthentication === ClientAuthentication.ClientSecretBasic) {
const basic = Buffer.from(`${this._clientId}:${this._clientSecret}`);

headers['Authorization'] = `Basic ${basic.toString('base64')}`;
}
if (this.clientAuthentication === ClientAuthentication.ClientSecretPost) {
values.client_secret = this._clientSecret;
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
}
const res = await this.transporter.request<CredentialRequest>({
...OAuth2Client.RETRY_CONFIG,
method: 'POST',
url,
data: querystring.stringify(values),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
headers,
});
const tokens = res.data as Credentials;
if (res.data && res.data.expires_in) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export {
RefreshOptions,
TokenInfo,
VerifyIdTokenOptions,
ClientAuthentication,
} from './auth/oauth2client';
export {LoginTicket, TokenPayload} from './auth/loginticket';
export {
Expand Down
94 changes: 93 additions & 1 deletion test/test.oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ import * as path from 'path';
import * as qs from 'querystring';
import * as sinon from 'sinon';

import {CodeChallengeMethod, Credentials, OAuth2Client} from '../src';
import {
CodeChallengeMethod,
Credentials,
OAuth2Client,
ClientAuthentication,
} from '../src';
import {LoginTicket} from '../src/auth/loginticket';

nock.disableNetConnect();
Expand Down Expand Up @@ -1366,6 +1371,7 @@ describe('oauth2', () => {
reqheaders: {'Content-Type': 'application/x-www-form-urlencoded'},
})
.post('/token')
.matchHeader('authorization', value => value === undefined)
.reply(200, {
access_token: 'abc',
refresh_token: '123',
Expand Down Expand Up @@ -1421,6 +1427,92 @@ describe('oauth2', () => {
assert.strictEqual(params.client_id, 'overridden');
});

it('getToken should use basic header auth if provided in options', async () => {
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
const authurl = 'https://sts.googleapis.com/v1/';
const basic_auth =
'Basic ' +
Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
const scope = nock(authurl)
.post('/oauthtoken')
.matchHeader('Authorization', basic_auth)
.reply(200, {
access_token: 'abc',
refresh_token: '123',
expires_in: 10,
});
const opts = {
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
redirectUri: REDIRECT_URI,
endpoints: {
oauth2AuthBaseUrl: 'https://auth.cloud.google/authorize',
oauth2TokenUrl: 'https://sts.googleapis.com/v1/oauthtoken',
tokenInfoUrl: 'https://sts.googleapis.com/v1/introspect',
},
clientAuthentication: ClientAuthentication.ClientSecretBasic,
};
const oauth2client = new OAuth2Client(opts);
const res = await oauth2client.getToken({
code: 'code here',
client_id: CLIENT_ID,
});
scope.done();
assert(res.res);
assert.equal(res.res.data.access_token, 'abc');
});

it('getToken should not use basic header auth if provided none in options and fail', async () => {
const authurl = 'https://some.example.auth/';
const scope = nock(authurl)
.post('/token')
.matchHeader('Authorization', val => val === undefined)
.reply(401);
const opts = {
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
redirectUri: REDIRECT_URI,
endpoints: {
oauth2AuthBaseUrl: 'https://auth.cloud.google/authorize',
oauth2TokenUrl: 'https://some.example.auth/token',
},
clientAuthentication: ClientAuthentication.None,
};
const oauth2client = new OAuth2Client(opts);
BigTailWolf marked this conversation as resolved.
Show resolved Hide resolved
assert.equal(
oauth2client.clientAuthentication,
ClientAuthentication.None
);

try {
await oauth2client.getToken({
code: 'code here',
client_id: CLIENT_ID,
});
throw new Error('Expected an error');
} catch (err) {
assert(err instanceof GaxiosError);
assert.equal(err.response?.status, 401);
} finally {
scope.done();
}
});

it('getToken should use auth secret post if not provided in options', async () => {
const opts = {
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
redirectUri: REDIRECT_URI,
endpoints: {
oauth2TokenUrl: 'mytokenurl',
},
};
const oauth2client = new OAuth2Client(opts);
assert.equal(
oauth2client.clientAuthentication,
ClientAuthentication.ClientSecretPost
);
});

it('should return expiry_date', done => {
const now = new Date().getTime();
const scope = nock(baseUrl, {
Expand Down