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

feat: Unify Base AuthClient Options #1663

Merged
merged 31 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
17a0bb2
feat: Expose `Gaxios` instance and default
danielbankhead Oct 5, 2023
e46e52d
feat: Unify Base `AuthClient` Options
danielbankhead Oct 5, 2023
5a76083
docs: clean up documentation
danielbankhead Oct 5, 2023
4537900
chore: Discourage external use via `@internal`
danielbankhead Oct 5, 2023
2990ab4
refactor: minor
danielbankhead Oct 5, 2023
efb563e
refactor: clean up `transporter` interface, options, and docs
danielbankhead Oct 5, 2023
5d54c94
Merge branch 'main' into authclient-enhancements
danielbankhead Oct 6, 2023
33898c5
Merge branch 'main' of github.com:googleapis/google-auth-library-node…
danielbankhead Oct 9, 2023
0c9df61
Merge branch 'main' of github.com:googleapis/google-auth-library-node…
danielbankhead Oct 10, 2023
690b6c7
Merge branch 'main' of github.com:googleapis/google-auth-library-node…
danielbankhead Oct 10, 2023
9803cfe
test: Init tests for `AuthClient`
danielbankhead Oct 10, 2023
e126bf3
fix: Use entire `JWT` to create accurate `createScoped`
danielbankhead Oct 11, 2023
d8e817b
chore: update `typescript` in fixtures
danielbankhead Oct 11, 2023
fad54d5
test: Use Sinon Fake Timer to Avoid Flaky Time Issue (#1667)
danielbankhead Oct 11, 2023
c1e28fb
Merge branch 'main' into authclient-enhancements
danielbankhead Oct 12, 2023
ef6aa2e
Merge branch 'main' into authclient-enhancements
danielbankhead Oct 12, 2023
f950465
docs(sample): Improve `keepAlive` sample with `transporterOptions`
danielbankhead Oct 16, 2023
2cbc9c4
docs: Docs-deprecate `additionalOptions`
danielbankhead Oct 20, 2023
d09348f
refactor: un-alias `getOriginalOrCamel`
danielbankhead Oct 20, 2023
88edee3
chore: remove confusing duplicate interface
danielbankhead Oct 20, 2023
aca07c6
docs: nit
danielbankhead Oct 20, 2023
52ba511
Merge branch 'main' into authclient-enhancements
danielbankhead Oct 23, 2023
26d5300
Merge branch 'main' of github.com:googleapis/google-auth-library-node…
danielbankhead Oct 25, 2023
aaf3545
docs: Improve camelCased option documentation
danielbankhead Oct 25, 2023
cac4b73
refactor: Unify `OriginalAndCamel` & `SnakeToCamelObject`
danielbankhead Oct 25, 2023
9af953f
docs: Add issue tracking link
danielbankhead Oct 25, 2023
f4c1cc8
refactor: Replace `getOriginalOrCamel` with a cleaner API
danielbankhead Oct 26, 2023
6a54219
feat: Allow optional `obj`
danielbankhead Oct 26, 2023
364af4d
refactor: re-add `SnakeToCamelObject`
danielbankhead Oct 26, 2023
8ea14f0
refactor: dedupe interface for now
danielbankhead Oct 26, 2023
99e1204
feat: Add camelCase options for `IdentityPoolClient`
danielbankhead Oct 26, 2023
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
17 changes: 11 additions & 6 deletions samples/keepalive.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,26 @@ const https = require('https');
* Acquire a client, and make a request to an API that's enabled by default.
*/
async function main() {
// create a new agent with keepAlive enabled
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
const agent = new https.Agent({keepAlive: true});

const auth = new GoogleAuth({
scopes: 'https://www.googleapis.com/auth/cloud-platform',
clientOptions: {
transporterOptions: {
agent,
},
},
});
const client = await auth.getClient();
const projectId = await auth.getProjectId();
const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`;

// create a new agent with keepAlive enabled
const agent = new https.Agent({keepAlive: true});

// use the agent as an Axios config param to make the request
const res = await client.request({url, agent});
// the agent uses the provided agent.
const res = await client.request({url});
console.log(res.data);

// Re-use the same agent to make the next request over the same connection
// Can also use another agent per-request.
const res2 = await client.request({url, agent});
console.log(res2.data);
}
Expand Down
110 changes: 97 additions & 13 deletions src/auth/authclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,91 @@
// limitations under the License.

import {EventEmitter} from 'events';
import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios';
import {Gaxios, GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios';

import {DefaultTransporter, Transporter} from '../transporters';
import {Credentials} from './credentials';
import {Headers} from './oauth2client';
import {OriginalAndCamel, getOriginalOrCamel as getOpt} from '../util';

/**
* Defines the root interface for all clients that generate credentials
* for calling Google APIs. All clients should implement this interface.
* Base auth configurations (e.g. from JWT or `.json` files)
*/
export interface CredentialsClient {
export interface AuthJSONOptions {
/**
* The project ID corresponding to the current credentials if available.
*/
projectId?: string | null;
project_id: string | null;

/**
* The expiration threshold in milliseconds before forcing token refresh.
* The quota project ID. The quota project can be used by client libraries for the billing purpose.
* See {@link https://cloud.google.com/docs/quota Working with quotas}
*/
eagerRefreshThresholdMillis: number;
quota_project_id: string;

/**
* Whether to force refresh on failure when making an authorization request.
* The default service domain for a given Cloud universe
*/
forceRefreshOnFailure: boolean;
universe_domain: string;
}

/**
* Base Auth Client configuration
*/
export type AuthClientOptions = Partial<
OriginalAndCamel<AuthJSONOptions> & {
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
credentials: Credentials;

/**
* A `Gaxios` or `Transporter` instance to use for `AuthClient` requests.
*/
transporter: Gaxios | Transporter;

/**
* Provides default options to the transporter, such as {@link GaxiosOptions.agent `agent`} or
* {@link GaxiosOptions.retryConfig `retryConfig`}.
*/
transporterOptions: GaxiosOptions;

/**
* The expiration threshold in milliseconds before forcing token refresh of
* unexpired tokens.
*/
eagerRefreshThresholdMillis: number;

/**
* Whether to attempt to refresh tokens on status 401/403 responses
* even if an attempt is made to refresh the token preemptively based
* on the expiry_date.
*/
forceRefreshOnFailure: boolean;
}
>;

/**
* The default cloud universe
*
* @see {@link AuthJSONOptions.universe_domain}
*/
export const DEFAULT_UNIVERSE = 'googleapis.com';

/**
* The default {@link AuthClientOptions.eagerRefreshThresholdMillis}
*/
export const DEFAULT_EAGER_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000;

/**
* Defines the root interface for all clients that generate credentials
* for calling Google APIs. All clients should implement this interface.
*/
export interface CredentialsClient {
projectId?: AuthClientOptions['projectId'];
eagerRefreshThresholdMillis: NonNullable<
AuthClientOptions['eagerRefreshThresholdMillis']
>;
forceRefreshOnFailure: NonNullable<
AuthClientOptions['forceRefreshOnFailure']
>;

/**
* @return A promise that resolves with the current GCP access token
Expand Down Expand Up @@ -88,16 +148,40 @@ export abstract class AuthClient
extends EventEmitter
implements CredentialsClient
{
projectId?: string | null;
/**
* The quota project ID. The quota project can be used by client libraries for the billing purpose.
* See {@link https://cloud.google.com/docs/quota| Working with quotas}
* See {@link https://cloud.google.com/docs/quota Working with quotas}
*/
quotaProjectId?: string;
transporter: Transporter = new DefaultTransporter();
transporter: Transporter;
credentials: Credentials = {};
projectId?: string | null;
eagerRefreshThresholdMillis = 5 * 60 * 1000;
eagerRefreshThresholdMillis = DEFAULT_EAGER_REFRESH_THRESHOLD_MILLIS;
forceRefreshOnFailure = false;
universeDomain = DEFAULT_UNIVERSE;

constructor(opts: AuthClientOptions = {}) {
super();

// Shared auth options
this.projectId = getOpt(opts, 'project_id') ?? null;
danielbankhead marked this conversation as resolved.
Show resolved Hide resolved
this.quotaProjectId = getOpt(opts, 'quota_project_id');
this.credentials = getOpt(opts, 'credentials') ?? {};
this.universeDomain = getOpt(opts, 'universe_domain') ?? DEFAULT_UNIVERSE;

// Shared client options
this.transporter = opts.transporter ?? new DefaultTransporter();
aeitzman marked this conversation as resolved.
Show resolved Hide resolved

if (opts.transporterOptions) {
this.transporter.defaults = opts.transporterOptions;
}

if (opts.eagerRefreshThresholdMillis) {
this.eagerRefreshThresholdMillis = opts.eagerRefreshThresholdMillis;
}

this.forceRefreshOnFailure = opts.forceRefreshOnFailure ?? false;
}

/**
* Provides an alternative Gaxios request implementation with auth credentials
Expand Down
8 changes: 6 additions & 2 deletions src/auth/awsclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
BaseExternalAccountClient,
BaseExternalAccountClientOptions,
} from './baseexternalclient';
import {RefreshOptions, Headers} from './oauth2client';
import {Headers} from './oauth2client';
import {AuthClientOptions} from './authclient';

/**
* AWS credentials JSON interface. This is used for AWS workloads.
Expand Down Expand Up @@ -85,7 +86,10 @@ export class AwsClient extends BaseExternalAccountClient {
* options. These currently customize expiration threshold time and
* whether to retry on 401/403 API request errors.
*/
constructor(options: AwsClientOptions, additionalOptions?: RefreshOptions) {
constructor(
options: AwsClientOptions,
additionalOptions?: AuthClientOptions
) {
super(options, additionalOptions);
this.environmentId = options.credential_source.environment_id;
// This is only required if the AWS region is not available in the
Expand Down
41 changes: 9 additions & 32 deletions src/auth/baseexternalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import {
import * as stream from 'stream';

import {Credentials} from './credentials';
import {AuthClient} from './authclient';
import {AuthClient, AuthClientOptions} from './authclient';
import {BodyResponseCallback} from '../transporters';
import {GetAccessTokenResponse, Headers, RefreshOptions} from './oauth2client';
import {GetAccessTokenResponse, Headers} from './oauth2client';
import * as sts from './stscredentials';
import {ClientAuthentication} from './oauth2common';

Expand Down Expand Up @@ -63,18 +63,13 @@ const WORKFORCE_AUDIENCE_PATTERN =
const pkg = require('../../../package.json');

/**
* The default cloud universe
* For backwards compatibility.
*/
export const DEFAULT_UNIVERSE = 'googleapis.com';
export {DEFAULT_UNIVERSE} from './authclient';

export interface SharedExternalAccountClientOptions {
export interface SharedExternalAccountClientOptions extends AuthClientOptions {
audience: string;
token_url: string;
quota_project_id?: string;
/**
* universe domain is the default service domain for a given Cloud universe
*/
universe_domain?: string;
}

/**
Expand Down Expand Up @@ -151,11 +146,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
private readonly stsCredential: sts.StsCredentials;
private readonly clientAuth?: ClientAuthentication;
private readonly workforcePoolUserProject?: string;
public universeDomain = DEFAULT_UNIVERSE;
public projectId: string | null;
public projectNumber: string | null;
public readonly eagerRefreshThresholdMillis: number;
public readonly forceRefreshOnFailure: boolean;
private readonly configLifetimeRequested: boolean;
protected credentialSourceType?: string;
/**
Expand All @@ -169,9 +160,10 @@ export abstract class BaseExternalAccountClient extends AuthClient {
*/
constructor(
options: BaseExternalAccountClientOptions,
additionalOptions?: RefreshOptions
additionalOptions?: AuthClientOptions
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
) {
super();
super({...options, ...additionalOptions});

if (options.type !== EXTERNAL_ACCOUNT_TYPE) {
throw new Error(
`Expected "${EXTERNAL_ACCOUNT_TYPE}" type but ` +
Expand All @@ -194,7 +186,6 @@ export abstract class BaseExternalAccountClient extends AuthClient {
this.cachedAccessToken = null;
this.audience = options.audience;
this.subjectTokenType = options.subject_token_type;
this.quotaProjectId = options.quota_project_id;
this.workforcePoolUserProject = options.workforce_pool_user_project;
const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN);
if (
Expand All @@ -217,22 +208,8 @@ export abstract class BaseExternalAccountClient extends AuthClient {
this.configLifetimeRequested = false;
this.serviceAccountImpersonationLifetime = DEFAULT_TOKEN_LIFESPAN;
}
// As threshold could be zero,
// eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the
// zero value.
if (typeof additionalOptions?.eagerRefreshThresholdMillis !== 'number') {
this.eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET;
} else {
this.eagerRefreshThresholdMillis = additionalOptions!
.eagerRefreshThresholdMillis as number;
}
this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure;
this.projectId = null;
this.projectNumber = this.getProjectNumber(this.audience);

if (options.universe_domain) {
this.universeDomain = options.universe_domain;
}
this.projectNumber = this.getProjectNumber(this.audience);
}

/** The service account email to be impersonated, if available. */
Expand Down
8 changes: 6 additions & 2 deletions src/auth/computeclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import {GaxiosError} from 'gaxios';
import * as gcpMetadata from 'gcp-metadata';

import {CredentialRequest, Credentials} from './credentials';
import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client';
import {
GetTokenResponse,
OAuth2Client,
OAuth2ClientOptions,
} from './oauth2client';

export interface ComputeOptions extends RefreshOptions {
export interface ComputeOptions extends OAuth2ClientOptions {
/**
* The service account email to use, or 'default'. A Compute Engine instance
* may have multiple service accounts.
Expand Down
24 changes: 5 additions & 19 deletions src/auth/downscopedclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import * as stream from 'stream';

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

import {GetAccessTokenResponse, Headers, RefreshOptions} from './oauth2client';
import {GetAccessTokenResponse, Headers} from './oauth2client';
import * as sts from './stscredentials';

/**
Expand Down Expand Up @@ -107,8 +107,6 @@ interface AvailabilityCondition {
export class DownscopedClient extends AuthClient {
private cachedDownscopedAccessToken: CredentialsWithResponse | null;
private readonly stsCredential: sts.StsCredentials;
public readonly eagerRefreshThresholdMillis: number;
public readonly forceRefreshOnFailure: boolean;

/**
* Instantiates a downscoped client object using the provided source
Expand All @@ -126,18 +124,17 @@ export class DownscopedClient extends AuthClient {
* permissions that are available on that resource and an optional
* condition to further restrict permissions.
* @param additionalOptions Optional additional behavior customization
* options. These currently customize expiration threshold time and
* whether to retry on 401/403 API request errors.
* options.
* @param quotaProjectId Optional quota project id for setting up in the
* x-goog-user-project header.
*/
constructor(
private readonly authClient: AuthClient,
private readonly credentialAccessBoundary: CredentialAccessBoundary,
additionalOptions?: RefreshOptions,
additionalOptions?: AuthClientOptions,
quotaProjectId?: string
) {
super();
super({...additionalOptions, quotaProjectId});
// Check 1-10 Access Boundary Rules are defined within Credential Access
// Boundary.
if (
Expand Down Expand Up @@ -167,17 +164,6 @@ export class DownscopedClient extends AuthClient {

this.stsCredential = new sts.StsCredentials(STS_ACCESS_TOKEN_URL);
this.cachedDownscopedAccessToken = null;
// As threshold could be zero,
// eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the
// zero value.
if (typeof additionalOptions?.eagerRefreshThresholdMillis !== 'number') {
this.eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET;
} else {
this.eagerRefreshThresholdMillis = additionalOptions!
.eagerRefreshThresholdMillis as number;
}
this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure;
this.quotaProjectId = quotaProjectId;
}

/**
Expand Down