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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: Remove Transporter #1937

Merged
merged 9 commits into from
Feb 7, 2025
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;
gaxios?: Gaxios;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting a 404 and I'm not seeing it on the main branch

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Np, those aren't the same transporters referenced in this PR.


/**
* Provides default options to the transporter, such as {@link GaxiosOptions.agent `agent`} or
* {@link GaxiosOptions.retryConfig `retryConfig`}.
*
* This option is ignored if {@link AuthClientOptions.gaxios `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.gaxios}
*
* @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.gaxios ?? 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
Loading