Skip to content

Commit

Permalink
refactor!: Remove Transporter (#1937)
Browse files Browse the repository at this point in the history
* refactor: remove `Transporter`

* chore: Migrate and deprecate `BodyResponseCallback`

* refactor!: Remove `Transporter`

* refactor: Use Gaxios Interceptors for default Auth headers

* refactor: keep old name to reduce refactoring downstream (if used)

* test: Add explicit tests for uniform auth headers

* style: lint
  • Loading branch information
d-goog authored Feb 7, 2025
1 parent 84d91b9 commit dbcc44b
Show file tree
Hide file tree
Showing 20 changed files with 333 additions and 409 deletions.
21 changes: 11 additions & 10 deletions samples/test/externalclient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ const {assert} = require('chai');
const {describe, it, before, afterEach} = require('mocha');
const fs = require('fs');
const {promisify} = require('util');
const {
GoogleAuth,
DefaultTransporter,
IdentityPoolClient,
} = require('google-auth-library');
const {GoogleAuth, IdentityPoolClient, gaxios} = require('google-auth-library');
const os = require('os');
const path = require('path');
const http = require('http');
Expand Down Expand Up @@ -158,11 +154,16 @@ const assumeRoleWithWebIdentity = async (
// been configured:
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html
const oidcToken = await generateGoogleIdToken(auth, aud, clientEmail);
const transporter = new DefaultTransporter();
const url =
'https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity' +
'&Version=2011-06-15&DurationSeconds=3600&RoleSessionName=nodejs-test' +
`&RoleArn=${awsRoleArn}&WebIdentityToken=${oidcToken}`;
const transporter = new gaxios.Gaxios();

const url = new URL('https://sts.amazonaws.com/');
url.searchParams.append('Action', 'AssumeRoleWithWebIdentity');
url.searchParams.append('Version', '2011-06-15');
url.searchParams.append('DurationSeconds', '3600');
url.searchParams.append('RoleSessionName', 'nodejs-test');
url.searchParams.append('RoleArn', awsRoleArn);
url.searchParams.append('WebIdentityToken', oidcToken);

// The response is in XML format but we will parse it as text.
const response = await transporter.request({url, responseType: 'text'});
const rawXml = response.data;
Expand Down
93 changes: 65 additions & 28 deletions src/auth/authclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
import {EventEmitter} from 'events';
import {Gaxios, GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios';

import {DefaultTransporter, Transporter} from '../transporters';
import {Credentials} from './credentials';
import {OriginalAndCamel, originalOrCamelOptions} from '../util';

import {PRODUCT_NAME, USER_AGENT} from '../shared.cjs';

/**
* Base auth configurations (e.g. from JWT or `.json` files) with conventional
* camelCased options.
Expand Down Expand Up @@ -81,13 +82,17 @@ export interface AuthClientOptions
credentials?: Credentials;

/**
* A `Gaxios` or `Transporter` instance to use for `AuthClient` requests.
* The {@link Gaxios `Gaxios`} instance used for making requests.
*
* @see {@link AuthClientOptions.useAuthRequestParameters}
*/
transporter?: Gaxios | Transporter;
transporter?: Gaxios;

/**
* Provides default options to the transporter, such as {@link GaxiosOptions.agent `agent`} or
* {@link GaxiosOptions.retryConfig `retryConfig`}.
*
* This option is ignored if {@link AuthClientOptions.transporter `gaxios`} has been provided
*/
transporterOptions?: GaxiosOptions;

Expand All @@ -103,6 +108,19 @@ export interface AuthClientOptions
* on the expiry_date.
*/
forceRefreshOnFailure?: boolean;

/**
* Enables/disables the adding of the AuthClient's default interceptor.
*
* @see {@link AuthClientOptions.transporter}
*
* @remarks
*
* Disabling is useful for debugging and experimentation.
*
* @default true
*/
useAuthRequestParameters?: boolean;
}

/**
Expand Down Expand Up @@ -183,7 +201,10 @@ export abstract class AuthClient
* See {@link https://cloud.google.com/docs/quota Working with quotas}
*/
quotaProjectId?: string;
transporter: Transporter;
/**
* The {@link Gaxios `Gaxios`} instance used for making requests.
*/
transporter: Gaxios;
credentials: Credentials = {};
eagerRefreshThresholdMillis = DEFAULT_EAGER_REFRESH_THRESHOLD_MILLIS;
forceRefreshOnFailure = false;
Expand All @@ -202,10 +223,12 @@ export abstract class AuthClient
this.universeDomain = options.get('universe_domain') ?? DEFAULT_UNIVERSE;

// Shared client options
this.transporter = opts.transporter ?? new DefaultTransporter();
this.transporter = opts.transporter ?? new Gaxios(opts.transporterOptions);

if (opts.transporterOptions) {
this.transporter.defaults = opts.transporterOptions;
if (options.get('useAuthRequestParameters') !== false) {
this.transporter.interceptors.request.add(
AuthClient.DEFAULT_REQUEST_INTERCEPTOR
);
}

if (opts.eagerRefreshThresholdMillis) {
Expand All @@ -216,29 +239,11 @@ export abstract class AuthClient
}

/**
* Return the {@link Gaxios `Gaxios`} instance from the {@link AuthClient.transporter}.
* The public request API in which credentials may be added to the request.
*
* @expiremental
*/
get gaxios(): Gaxios | null {
if (this.transporter instanceof Gaxios) {
return this.transporter;
} else if (this.transporter instanceof DefaultTransporter) {
return this.transporter.instance;
} else if (
'instance' in this.transporter &&
this.transporter.instance instanceof Gaxios
) {
return this.transporter.instance;
}

return null;
}

/**
* Provides an alternative Gaxios request implementation with auth credentials
* @param options options for `gaxios`
*/
abstract request<T>(opts: GaxiosOptions): GaxiosPromise<T>;
abstract request<T>(options: GaxiosOptions): GaxiosPromise<T>;

/**
* The main authentication interface. It takes an optional url which when
Expand Down Expand Up @@ -288,6 +293,31 @@ export abstract class AuthClient
return headers;
}

static readonly DEFAULT_REQUEST_INTERCEPTOR: Parameters<
Gaxios['interceptors']['request']['add']
>[0] = {
resolved: async config => {
const headers = config.headers || {};

// Set `x-goog-api-client`, if not already set
if (!headers['x-goog-api-client']) {
const nodeVersion = process.version.replace(/^v/, '');
headers['x-goog-api-client'] = `gl-node/${nodeVersion}`;
}

// Set `User-Agent`
if (!headers['User-Agent']) {
headers['User-Agent'] = USER_AGENT;
} else if (!headers['User-Agent'].includes(`${PRODUCT_NAME}/`)) {
headers['User-Agent'] = `${headers['User-Agent']} ${USER_AGENT}`;
}

config.headers = headers;

return config;
},
};

/**
* Retry config for Auth-related requests.
*
Expand Down Expand Up @@ -315,3 +345,10 @@ export interface GetAccessTokenResponse {
token?: string | null;
res?: GaxiosResponse | null;
}

/**
* @deprecated - use the Promise API instead
*/
export interface BodyResponseCallback<T> {
(err: Error | null, res?: GaxiosResponse<T> | null): void;
}
14 changes: 9 additions & 5 deletions src/auth/baseexternalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import {
AuthClientOptions,
GetAccessTokenResponse,
Headers,
BodyResponseCallback,
} from './authclient';
import {BodyResponseCallback, Transporter} from '../transporters';
import * as sts from './stscredentials';
import {ClientAuthentication} from './oauth2common';
import {SnakeToCamelObject, originalOrCamelOptions} from '../util';
Expand Down Expand Up @@ -110,10 +110,11 @@ export interface ExternalAccountSupplierContext {
* * "urn:ietf:params:oauth:token-type:id_token"
*/
subjectTokenType: string;
/** The {@link Gaxios} or {@link Transporter} instance from
* the calling external account to use for requests.
/**
* The {@link Gaxios} instance for calling external account
* to use for requests.
*/
transporter: Transporter | Gaxios;
transporter: Gaxios;
}

/**
Expand Down Expand Up @@ -312,7 +313,10 @@ export abstract class BaseExternalAccountClient extends AuthClient {
};
}

this.stsCredential = new sts.StsCredentials(tokenUrl, this.clientAuth);
this.stsCredential = new sts.StsCredentials({
tokenExchangeEndpoint: tokenUrl,
clientAuthentication: this.clientAuth,
});
this.scopes = opts.get('scopes') || [DEFAULT_OAUTH_SCOPE];
this.cachedAccessToken = null;
this.audience = opts.get('audience');
Expand Down
9 changes: 3 additions & 6 deletions src/auth/defaultawssecuritycredentialssupplier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import {ExternalAccountSupplierContext} from './baseexternalclient';
import {Gaxios, GaxiosOptions} from 'gaxios';
import {Transporter} from '../transporters';
import {AwsSecurityCredentialsSupplier} from './awsclient';
import {AwsSecurityCredentials} from './awsrequestsigner';
import {Headers} from './authclient';
Expand Down Expand Up @@ -183,9 +182,7 @@ export class DefaultAwsSecurityCredentialsSupplier
* @param transporter The transporter to use for requests.
* @return A promise that resolves with the IMDSv2 Session Token.
*/
async #getImdsV2SessionToken(
transporter: Transporter | Gaxios
): Promise<string> {
async #getImdsV2SessionToken(transporter: Gaxios): Promise<string> {
const opts: GaxiosOptions = {
...this.additionalGaxiosOptions,
url: this.imdsV2SessionTokenUrl,
Expand All @@ -205,7 +202,7 @@ export class DefaultAwsSecurityCredentialsSupplier
*/
async #getAwsRoleName(
headers: Headers,
transporter: Transporter | Gaxios
transporter: Gaxios
): Promise<string> {
if (!this.securityCredentialsUrl) {
throw new Error(
Expand Down Expand Up @@ -236,7 +233,7 @@ export class DefaultAwsSecurityCredentialsSupplier
async #retrieveAwsSecurityCredentials(
roleName: string,
headers: Headers,
transporter: Transporter | Gaxios
transporter: Gaxios
): Promise<AwsSecurityCredentialsResponse> {
const response = await transporter.request<AwsSecurityCredentialsResponse>({
...this.additionalGaxiosOptions,
Expand Down
8 changes: 4 additions & 4 deletions src/auth/downscopedclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import {
} from 'gaxios';
import * as stream from 'stream';

import {BodyResponseCallback} from '../transporters';
import {Credentials} from './credentials';
import {
AuthClient,
AuthClientOptions,
GetAccessTokenResponse,
Headers,
BodyResponseCallback,
} from './authclient';

import * as sts from './stscredentials';
Expand Down Expand Up @@ -189,9 +189,9 @@ export class DownscopedClient extends AuthClient {
}
}

this.stsCredential = new sts.StsCredentials(
`https://sts.${this.universeDomain}/v1/token`
);
this.stsCredential = new sts.StsCredentials({
tokenExchangeEndpoint: `https://sts.${this.universeDomain}/v1/token`,
});

this.cachedDownscopedAccessToken = null;
}
Expand Down
39 changes: 24 additions & 15 deletions src/auth/externalAccountAuthorizedUserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {AuthClient, Headers} from './authclient';
import {AuthClient, Headers, BodyResponseCallback} from './authclient';
import {
ClientAuthentication,
getErrorFromOAuthErrorResponse,
OAuthClientAuthHandler,
OAuthClientAuthHandlerOptions,
OAuthErrorResponse,
} from './oauth2common';
import {BodyResponseCallback, Transporter} from '../transporters';
import {
GaxiosError,
GaxiosOptions,
Expand Down Expand Up @@ -69,24 +69,32 @@ interface TokenRefreshResponse {
res?: GaxiosResponse | null;
}

interface ExternalAccountAuthorizedUserHandlerOptions
extends OAuthClientAuthHandlerOptions {
/**
* The URL of the token refresh endpoint.
*/
tokenRefreshEndpoint: string | URL;
}

/**
* Handler for token refresh requests sent to the token_url endpoint for external
* authorized user credentials.
*/
class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler {
#tokenRefreshEndpoint: string | URL;

/**
* Initializes an ExternalAccountAuthorizedUserHandler instance.
* @param url The URL of the token refresh endpoint.
* @param transporter The transporter to use for the refresh request.
* @param clientAuthentication The client authentication credentials to use
* for the refresh request.
*/
constructor(
private readonly url: string,
private readonly transporter: Transporter,
clientAuthentication?: ClientAuthentication
) {
super(clientAuthentication);
constructor(options: ExternalAccountAuthorizedUserHandlerOptions) {
super(options);

this.#tokenRefreshEndpoint = options.tokenRefreshEndpoint;
}

/**
Expand Down Expand Up @@ -114,7 +122,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler {

const opts: GaxiosOptions = {
...ExternalAccountAuthorizedUserHandler.RETRY_CONFIG,
url: this.url,
url: this.#tokenRefreshEndpoint,
method: 'POST',
headers,
data: values.toString(),
Expand Down Expand Up @@ -169,18 +177,19 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient {
this.universeDomain = options.universe_domain;
}
this.refreshToken = options.refresh_token;
const clientAuth = {
const clientAuthentication = {
confidentialClientType: 'basic',
clientId: options.client_id,
clientSecret: options.client_secret,
} as ClientAuthentication;
this.externalAccountAuthorizedUserHandler =
new ExternalAccountAuthorizedUserHandler(
options.token_url ??
new ExternalAccountAuthorizedUserHandler({
tokenRefreshEndpoint:
options.token_url ??
DEFAULT_TOKEN_URL.replace('{universeDomain}', this.universeDomain),
this.transporter,
clientAuth
);
transporter: this.transporter,
clientAuthentication,
});

this.cachedAccessToken = null;
this.quotaProjectId = options.quota_project_id;
Expand Down
2 changes: 1 addition & 1 deletion src/auth/oauth2client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import * as stream from 'stream';
import * as formatEcdsa from 'ecdsa-sig-formatter';

import {createCrypto, JwkCertificate, hasBrowserCrypto} from '../crypto/crypto';
import {BodyResponseCallback} from '../transporters';

import {
AuthClient,
AuthClientOptions,
GetAccessTokenResponse,
Headers,
BodyResponseCallback,
} from './authclient';
import {CredentialRequest, Credentials} from './credentials';
import {LoginTicket, TokenPayload} from './loginticket';
Expand Down
Loading

0 comments on commit dbcc44b

Please sign in to comment.