Skip to content

Commit

Permalink
feat: Adding support of client authentication method. (#1814)
Browse files Browse the repository at this point in the history
* 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 <12433791+aeitzman@users.noreply.github.com>

* Update src/auth/oauth2client.ts

Co-authored-by: aeitzman <12433791+aeitzman@users.noreply.github.com>

* fix the 'any' type into strict check

* fix lint

* fix lint

* Update src/auth/oauth2client.ts

Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>

* Update src/auth/oauth2client.ts

Co-authored-by: aeitzman <12433791+aeitzman@users.noreply.github.com>

* address comments

* fix tests

* fix tests

* fix tests and lint

* Update src/auth/oauth2client.ts

Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>

* addressing comments

* adding validation of no auth header

* Update src/auth/oauth2client.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update src/auth/oauth2client.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update test/test.oauth2.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update test/test.oauth2.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* Update test/test.oauth2.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* fix CI after apply changes

* Update src/auth/oauth2client.ts

Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>

* fix syntax of post value

* fix lint

* fix the client_secret field

* refactor: interface and readability

---------

Co-authored-by: aeitzman <12433791+aeitzman@users.noreply.github.com>
Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>
Co-authored-by: Daniel Bankhead <dan@danielbankhead.com>
Co-authored-by: Daniel Bankhead <danielbankhead@google.com>
  • Loading branch information
5 people committed Jun 1, 2024
1 parent 0202365 commit 4a14e8c
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 7 deletions.
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 {
ClientSecretPost = 'ClientSecretPost',
ClientSecretBasic = 'ClientSecretBasic',
None = 'None',
}

export interface GetTokenOptions {
code: string;
codeVerifier?: string;
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;
}
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 () => {
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);
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

0 comments on commit 4a14e8c

Please sign in to comment.