From 8769c7934cf130c132d7ec0dec994575d394d884 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:51:35 -0400 Subject: [PATCH] chore: revert "feat(auth): Regional access boundaries main merge (#8665)" This reverts commit 76e6d3b6dea6032547780155c045e41552e27f40. --- .../src/auth/authclient.ts | 88 +----- .../src/auth/baseexternalclient.ts | 84 ++---- .../src/auth/computeclient.ts | 69 ----- .../src/auth/downscopedclient.ts | 16 +- .../externalAccountAuthorizedUserClient.ts | 55 +--- .../src/auth/idtokenclient.ts | 9 +- .../src/auth/impersonated.ts | 25 -- .../src/auth/jwtclient.ts | 27 +- .../src/auth/oauth2client.ts | 29 +- .../src/auth/regionalaccessboundary.ts | 268 ---------------- .../google-auth-library-nodejs/src/util.ts | 37 --- .../test/test.authclient.ts | 285 +----------------- .../test/test.baseexternalclient.ts | 246 --------------- .../test/test.compute.ts | 199 +----------- ...est.externalaccountauthorizeduserclient.ts | 99 +----- .../test/test.externalclient.ts | 71 +++-- .../test/test.impersonated.ts | 115 ------- .../test/test.jwt.ts | 172 ----------- .../google-auth-library-nodejs/tsconfig.json | 5 +- 19 files changed, 115 insertions(+), 1784 deletions(-) delete mode 100644 core/packages/google-auth-library-nodejs/src/auth/regionalaccessboundary.ts diff --git a/core/packages/google-auth-library-nodejs/src/auth/authclient.ts b/core/packages/google-auth-library-nodejs/src/auth/authclient.ts index 216e3390fdc..f18dd58e2ab 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/authclient.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/authclient.ts @@ -20,10 +20,6 @@ import {OriginalAndCamel, originalOrCamelOptions} from '../util'; import {log as makeLog} from 'google-logging-utils'; import {PRODUCT_NAME, USER_AGENT} from '../shared.cjs'; -import { - RegionalAccessBoundaryData, - RegionalAccessBoundaryManager, -} from './regionalaccessboundary'; /** * An interface for enforcing `fetch`-type compliance. @@ -236,7 +232,6 @@ export abstract class AuthClient eagerRefreshThresholdMillis = DEFAULT_EAGER_REFRESH_THRESHOLD_MILLIS; forceRefreshOnFailure = false; universeDomain = DEFAULT_UNIVERSE; - protected regionalAccessBoundaryManager: RegionalAccessBoundaryManager; /** * Symbols that can be added to GaxiosOptions to specify the method name that is @@ -263,12 +258,6 @@ export abstract class AuthClient // Shared client options this.transporter = opts.transporter ?? new Gaxios(opts.transporterOptions); - this.regionalAccessBoundaryManager = new RegionalAccessBoundaryManager({ - transporter: this.transporter, - getLookupUrl: async () => this.getRegionalAccessBoundaryUrl(), - isUniverseDomainDefault: () => this.universeDomain === DEFAULT_UNIVERSE, - }); - if (options.get('useAuthRequestParameters') !== false) { this.transporter.interceptors.request.add( AuthClient.DEFAULT_REQUEST_INTERCEPTOR, @@ -372,21 +361,6 @@ export abstract class AuthClient res?: GaxiosResponse | null; }>; - /** - * Returns the regional access boundary lookup URL for the current client. - * This method is intended for internal use by the RegionalAccessBoundaryManager - * and should not be called directly by users. - * - * @return The regional access boundary URL string, or `null` if the client type - * does not support regional access boundaries. - * @throws {Error} If the URL cannot be constructed for a compatible client, - * for instance, if a required property like a service account email is missing. - * @internal - */ - public async getRegionalAccessBoundaryUrl(): Promise { - return null; - } - /** * Sets the auth credentials. */ @@ -394,22 +368,6 @@ export abstract class AuthClient this.credentials = credentials; } - /** - * Returns the current regional access boundary data. - * @internal - */ - getRegionalAccessBoundary(): RegionalAccessBoundaryData | null { - return this.regionalAccessBoundaryManager.data; - } - - /** - * Returns the current regional access boundary cooldown time in milliseconds. - * @internal - */ - getRegionalAccessBoundaryCooldownTime(): number { - return this.regionalAccessBoundaryManager.cooldownTime; - } - /** * Append additional headers, e.g., x-goog-user-project, shared across the * classes inheriting AuthClient. This method should be used by any method @@ -428,47 +386,23 @@ export abstract class AuthClient ) { headers.set('x-goog-user-project', this.quotaProjectId); } - return headers; } /** - * Applies regional access boundary rules to the provided headers. - * This includes adding the x-allowed-locations header and triggering - * a background refresh if needed. - * @param headers The headers to update. - * @param url Optional destination URL of the request. If missing, assumed global. - */ - protected applyRegionalAccessBoundary( - headers: Headers, - url?: string | URL, - ): void { - const rabHeader = - this.regionalAccessBoundaryManager.getRegionalAccessBoundaryHeader( - url, - headers, - ); - if (rabHeader) { - headers.set('x-allowed-locations', rabHeader); - } - } - - /** - * Adds the `x-goog-user-project`, `authorization`, and 'x-allowed-locations' - * headers to the target Headers + * Adds the `x-goog-user-project` and `authorization` headers to the target Headers * object, if they exist on the source. * * @param target the headers to target * @param source the headers to source from * @returns the target headers */ - protected applyHeadersFromSource( + protected addUserProjectAndAuthHeaders( target: T, source: Headers, ): T { const xGoogUserProject = source.get('x-goog-user-project'); const authorizationHeader = source.get('authorization'); - const xGoogAllowedLocs = source.get('x-allowed-locations'); if (xGoogUserProject) { target.set('x-goog-user-project', xGoogUserProject); @@ -478,10 +412,6 @@ export abstract class AuthClient target.set('authorization', authorizationHeader); } - if (xGoogAllowedLocs) { - target.set('x-allowed-locations', xGoogAllowedLocs); - } - return target; } @@ -619,20 +549,6 @@ export abstract class AuthClient }, }; } - - /** - * Returns whether the provided credentials are expired or will expire within - * eagerRefreshThresholdMillismilliseconds. - * If there is no expiry time, assumes the token is not expired or expiring. - * @param credentials The credentials to check for expiration. - * @return Whether the credentials are expired or not. - */ - protected isExpired(credentials: Credentials = this.credentials): boolean { - const now = new Date().getTime(); - return credentials.expiry_date - ? now >= credentials.expiry_date - this.eagerRefreshThresholdMillis - : false; - } } // TypeScript does not have `HeadersInit` in the standard types yet diff --git a/core/packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts b/core/packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts index f569a590fc2..48111eb6345 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts @@ -31,16 +31,7 @@ import { import * as sts from './stscredentials'; import {ClientAuthentication} from './oauth2common'; import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; -import { - getWorkforcePoolIdFromAudience, - getWorkloadPoolIdFromAudience, -} from '../util'; import {pkg} from '../shared.cjs'; -import { - SERVICE_ACCOUNT_LOOKUP_ENDPOINT, - WORKFORCE_LOOKUP_ENDPOINT, - WORKLOAD_LOOKUP_ENDPOINT, -} from './regionalaccessboundary'; /** * The required token exchange grant_type: rfc8693#section-2.1 @@ -424,12 +415,11 @@ export abstract class BaseExternalAccountClient extends AuthClient { * The result has the form: * { authorization: 'Bearer ' } */ - async getRequestHeaders(url?: string | URL): Promise { + async getRequestHeaders(): Promise { const accessTokenResponse = await this.getAccessToken(); const headers = new Headers({ authorization: `Bearer ${accessTokenResponse.token}`, }); - this.applyRegionalAccessBoundary(headers, url); return this.addSharedMetadataHeaders(headers); } @@ -509,14 +499,13 @@ export abstract class BaseExternalAccountClient extends AuthClient { reAuthRetried = false, ): Promise> { let response: GaxiosResponse; - const requestOpts = {...opts}; try { - const requestHeaders = await this.getRequestHeaders(opts.url); - requestOpts.headers = Gaxios.mergeHeaders(requestOpts.headers); + const requestHeaders = await this.getRequestHeaders(); + opts.headers = Gaxios.mergeHeaders(opts.headers); - this.applyHeadersFromSource(requestOpts.headers, requestHeaders); + this.addUserProjectAndAuthHeaders(opts.headers, requestHeaders); - response = await this.transporter.request(requestOpts); + response = await this.transporter.request(opts); } catch (e) { const res = (e as GaxiosError).response; if (res) { @@ -695,6 +684,19 @@ export abstract class BaseExternalAccountClient extends AuthClient { }; } + /** + * Returns whether the provided credentials are expired or not. + * If there is no expiry time, assumes the token is not expired or expiring. + * @param accessToken The credentials to check for expiration. + * @return Whether the credentials are expired or not. + */ + private isExpired(accessToken: Credentials): boolean { + const now = new Date().getTime(); + return accessToken.expiry_date + ? now >= accessToken.expiry_date - this.eagerRefreshThresholdMillis + : false; + } + /** * @return The list of scopes for the requested GCP access token. */ @@ -720,54 +722,4 @@ export abstract class BaseExternalAccountClient extends AuthClient { protected getTokenUrl(): string { return this.tokenUrl; } - - /** - * Returns the regional access boundary lookup URL for the external account. - * This implementation constructs the URL based on the audience of the - * workforce or workload pool. If the client is configured for service account - * impersonation, it uses the target service account email to generate - * the lookup endpoint. - * - * @return The regional access boundary URL string. - * @internal - */ - public async getRegionalAccessBoundaryUrl(): Promise { - if (this.serviceAccountImpersonationUrl) { - // When impersonating a service account, the regional access boundary is determined - // by the security policies of the target service account. - const email = this.getServiceAccountEmail(); - if (!email) { - throw new Error( - `RegionalAccessBoundary: A service account email is required for regional access boundary lookups but could not be determined from the serviceAccountImpersonationUrl ${this.serviceAccountImpersonationUrl}.`, - ); - } - return SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(email), - ); - } - - // Check if the audience corresponds to a workload identity pool. - const wfPoolId = getWorkforcePoolIdFromAudience(this.audience); - if (wfPoolId) { - return WORKFORCE_LOOKUP_ENDPOINT.replace( - '{pool_id}', - encodeURIComponent(wfPoolId), - ); - } - - // Check if the audience corresponds to a workforce identity pool. - const wlPoolId = getWorkloadPoolIdFromAudience(this.audience); - const projectNumber = this.getProjectNumber(this.audience); - if (wlPoolId && projectNumber) { - return WORKLOAD_LOOKUP_ENDPOINT.replace( - '{project_id}', - projectNumber, - ).replace('{pool_id}', wlPoolId); - } - - throw new RangeError( - `RegionalAccessBoundary: Invalid audience provided: "${this.audience}" does not correspond to a workforce or workload pool.`, - ); - } } diff --git a/core/packages/google-auth-library-nodejs/src/auth/computeclient.ts b/core/packages/google-auth-library-nodejs/src/auth/computeclient.ts index 4729e2c9df3..36ca73d172a 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/computeclient.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/computeclient.ts @@ -15,14 +15,12 @@ import {GaxiosError} from 'gaxios'; import * as gcpMetadata from 'gcp-metadata'; -import {AuthClient} from './authclient'; import {CredentialRequest, Credentials} from './credentials'; import { GetTokenResponse, OAuth2Client, OAuth2ClientOptions, } from './oauth2client'; -import {SERVICE_ACCOUNT_LOOKUP_ENDPOINT} from './regionalaccessboundary'; export interface ComputeOptions extends OAuth2ClientOptions { /** @@ -39,10 +37,8 @@ export interface ComputeOptions extends OAuth2ClientOptions { } export class Compute extends OAuth2Client { - private static readonly EMAIL_REGEX = /^[^@]+@[^@]+\.[^@]+$/; readonly serviceAccountEmail: string; scopes: string[]; - private isNonEmailAccount = false; /** * Google Compute Engine service account credentials. @@ -141,69 +137,4 @@ export class Compute extends OAuth2Client { } } } - - /** - * Returns the regional access boundary lookup URL for the GCE instance. - * This implementation resolves the service account email of the GCE - * instance to construct the lookup endpoint. If the resolved email is invalid - * or not found, it returns `null` to skip the regional access boundary check. - * - * @return The regional access boundary URL string, or null if regional access - * boundary checks should be skipped. - * @internal - */ - public async getRegionalAccessBoundaryUrl(): Promise { - const email = await this.resolveServiceAccountEmail(); - if (email === null) { - // This credential corresponds to a non-email account; skip RAB lookup. - return null; - } - const regionalAccessBoundaryUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(email), - ); - return regionalAccessBoundaryUrl; - } - - /** - * Resolves the service account email. If the email is set to 'default', - * it fetches the email from the GCE metadata server. - * @returns A promise that resolves with the service account email, - * or null if MDS returns an invalid email format - */ - private async resolveServiceAccountEmail(): Promise { - if (this.isNonEmailAccount) { - return null; - } - - if (this.serviceAccountEmail !== 'default') { - // If a specific email is provided, return it directly. - return this.serviceAccountEmail; - } - - // Otherwise, fetch the default email from the metadata server. - try { - const email = await gcpMetadata.instance( - 'service-accounts/default/email', - ); - - // If the metadata server returned an non-email format, log a warning only once. - if (!email || !Compute.EMAIL_REGEX.test(email)) { - AuthClient.log.info( - `RegionalAccessBoundary: Service account email "${email}" is not in a valid email format. Skipping regional access boundary lookup.`, - ); - this.isNonEmailAccount = true; - return null; - } - - return email; - } catch (e) { - throw new Error( - 'RegionalAccessBoundary: Failed to retrieve default service account email from metadata server.', - { - cause: e, - }, - ); - } - } } diff --git a/core/packages/google-auth-library-nodejs/src/auth/downscopedclient.ts b/core/packages/google-auth-library-nodejs/src/auth/downscopedclient.ts index cbb6a1e255b..bc0d19b16b8 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/downscopedclient.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/downscopedclient.ts @@ -290,7 +290,7 @@ export class DownscopedClient extends AuthClient { const requestHeaders = await this.getRequestHeaders(); opts.headers = Gaxios.mergeHeaders(opts.headers); - this.applyHeadersFromSource(opts.headers, requestHeaders); + this.addUserProjectAndAuthHeaders(opts.headers, requestHeaders); response = await this.transporter.request(opts); } catch (e) { @@ -381,4 +381,18 @@ export class DownscopedClient extends AuthClient { // Return the cached access token. return this.cachedDownscopedAccessToken; } + + /** + * Returns whether the provided credentials are expired or not. + * If there is no expiry time, assumes the token is not expired or expiring. + * @param downscopedAccessToken The credentials to check for expiration. + * @return Whether the credentials are expired or not. + */ + private isExpired(downscopedAccessToken: Credentials): boolean { + const now = new Date().getTime(); + return downscopedAccessToken.expiry_date + ? now >= + downscopedAccessToken.expiry_date - this.eagerRefreshThresholdMillis + : false; + } } diff --git a/core/packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts b/core/packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts index 59d8f42f6f4..320db05546a 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts @@ -33,8 +33,6 @@ import { EXPIRATION_TIME_OFFSET, SharedExternalAccountClientOptions, } from './baseexternalclient'; -import {WORKFORCE_LOOKUP_ENDPOINT} from './regionalaccessboundary'; -import {getWorkforcePoolIdFromAudience} from '../util'; /** * The credentials JSON file type for external account authorized user clients. @@ -161,7 +159,6 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { private cachedAccessToken: CredentialsWithResponse | null; private readonly externalAccountAuthorizedUserHandler: ExternalAccountAuthorizedUserHandler; private refreshToken: string; - private readonly audience: string; /** * Instantiates an ExternalAccountAuthorizedUserClient instances using the @@ -175,7 +172,6 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { if (options.universe_domain) { this.universeDomain = options.universe_domain; } - this.audience = options.audience; this.refreshToken = options.refresh_token; const clientAuthentication = { confidentialClientType: 'basic', @@ -222,20 +218,11 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { }; } - /** - * The main authentication interface. It takes an optional url which when - * present is the endpoint being accessed, and returns a Promise which - * resolves with authorization header fields. - * - * @param url The URI being authorized. - * @returns A promise that resolves with authorization header fields. - */ - async getRequestHeaders(url?: string | URL): Promise { + async getRequestHeaders(): Promise { const accessTokenResponse = await this.getAccessToken(); const headers = new Headers({ authorization: `Bearer ${accessTokenResponse.token}`, }); - this.applyRegionalAccessBoundary(headers, url); return this.addSharedMetadataHeaders(headers); } @@ -269,14 +256,13 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { reAuthRetried = false, ): Promise> { let response: GaxiosResponse; - const requestOpts = {...opts}; try { - const requestHeaders = await this.getRequestHeaders(opts.url); - requestOpts.headers = Gaxios.mergeHeaders(requestOpts.headers); + const requestHeaders = await this.getRequestHeaders(); + opts.headers = Gaxios.mergeHeaders(opts.headers); - this.applyHeadersFromSource(requestOpts.headers, requestHeaders); + this.addUserProjectAndAuthHeaders(opts.headers, requestHeaders); - response = await this.transporter.request(requestOpts); + response = await this.transporter.request(opts); } catch (e) { const res = (e as GaxiosError).response; if (res) { @@ -322,34 +308,21 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { if (refreshResponse.refresh_token !== undefined) { this.refreshToken = refreshResponse.refresh_token; - - // Set credentials. - this.credentials = {...this.cachedAccessToken}; - delete (this.credentials as CredentialsWithResponse).res; } return this.cachedAccessToken; } /** - * Returns the regional access boundary lookup URL for the external account - * authorized user. - * This implementation constructs the lookup endpoint using the workforce - * pool ID resolved from the audience. - * - * @return The regional access boundary URL string. - * @internal + * Returns whether the provided credentials are expired or not. + * If there is no expiry time, assumes the token is not expired or expiring. + * @param credentials The credentials to check for expiration. + * @return Whether the credentials are expired or not. */ - public async getRegionalAccessBoundaryUrl(): Promise { - const poolId = getWorkforcePoolIdFromAudience(this.audience); - if (!poolId) { - throw new Error( - `RegionalAccessBoundary: A workforce pool ID is required for regional access boundary lookups but could not be determined from the audience: ${this.audience}.`, - ); - } - return WORKFORCE_LOOKUP_ENDPOINT.replace( - '{pool_id}', - encodeURIComponent(poolId), - ); + private isExpired(credentials: Credentials): boolean { + const now = new Date().getTime(); + return credentials.expiry_date + ? now >= credentials.expiry_date - this.eagerRefreshThresholdMillis + : false; } } diff --git a/core/packages/google-auth-library-nodejs/src/auth/idtokenclient.ts b/core/packages/google-auth-library-nodejs/src/auth/idtokenclient.ts index 58ed71ae210..68303c97e36 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/idtokenclient.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/idtokenclient.ts @@ -54,7 +54,7 @@ export class IdTokenClient extends OAuth2Client { if ( !this.credentials.id_token || !this.credentials.expiry_date || - this.isExpired() + this.isTokenExpiring() ) { const idToken = await this.idTokenProvider.fetchIdToken( this.targetAudience, @@ -68,12 +68,7 @@ export class IdTokenClient extends OAuth2Client { const headers = new Headers({ authorization: 'Bearer ' + this.credentials.id_token, }); - return { - headers, - // Since ID-tokens are outside RAB scope, isIDToken is used as a flag - // to avoid RAB lookup. - isIDToken: true, - }; + return {headers}; } private getIdTokenExpiryDate(idToken: string): number | void { diff --git a/core/packages/google-auth-library-nodejs/src/auth/impersonated.ts b/core/packages/google-auth-library-nodejs/src/auth/impersonated.ts index d146c11f31d..97742ef668b 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/impersonated.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/impersonated.ts @@ -24,7 +24,6 @@ import {IdTokenProvider} from './idtokenclient'; import {GaxiosError} from 'gaxios'; import {SignBlobResponse} from './googleauth'; import {originalOrCamelOptions} from '../util'; -import {SERVICE_ACCOUNT_LOOKUP_ENDPOINT} from './regionalaccessboundary'; export interface ImpersonatedOptions extends OAuth2ClientOptions { /** @@ -203,7 +202,6 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { const tokenResponse = res.data; this.credentials.access_token = tokenResponse.accessToken; this.credentials.expiry_date = Date.parse(tokenResponse.expireTime); - return { tokens: this.credentials, res, @@ -262,27 +260,4 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { return res.data.token; } - - /** - * Returns the regional access boundary lookup URL for the impersonated - * service account. - * This implementation uses the target principal (service account email) - * to construct the lookup endpoint. - * - * @return The regional access boundary URL string. - * @internal - */ - public async getRegionalAccessBoundaryUrl(): Promise { - const targetPrincipal = this.getTargetPrincipal(); - if (!targetPrincipal) { - throw new Error( - 'RegionalAccessBoundary: A targetPrincipal is required for regional access boundary lookups but was not provided in the ImpersonatedClient options.', - ); - } - const regionalAccessBoundaryUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(targetPrincipal), - ); - return regionalAccessBoundaryUrl; - } } diff --git a/core/packages/google-auth-library-nodejs/src/auth/jwtclient.ts b/core/packages/google-auth-library-nodejs/src/auth/jwtclient.ts index 2f5c0bda0ec..55ce73e849e 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/jwtclient.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/jwtclient.ts @@ -26,7 +26,6 @@ import { RequestMetadataResponse, } from './oauth2client'; import {DEFAULT_UNIVERSE} from './authclient'; -import {SERVICE_ACCOUNT_LOOKUP_ENDPOINT} from './regionalaccessboundary'; export interface JWTOptions extends OAuth2ClientOptions { /** @@ -148,9 +147,6 @@ export class JWT extends OAuth2Client implements IdTokenProvider { authorization: `Bearer ${tokens.id_token}`, }), ), - // Since ID-tokens are outside RAB scope, - // isIDToken is used as a flag to avoid RAB lookup. - isIDToken: true, }; } else { // no scopes have been set, but a uri has been provided. Use JWTAccess @@ -275,7 +271,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { protected async refreshTokenNoCache(): Promise { const gtoken = this.createGToken(); const token = await gtoken.getToken({ - forceRefresh: this.isExpired(), + forceRefresh: this.isTokenExpiring(), }); const tokens = { access_token: token.access_token, @@ -412,25 +408,4 @@ export class JWT extends OAuth2Client implements IdTokenProvider { } throw new Error('A key or a keyFile must be provided to getCredentials.'); } - - /** - * Returns the regional access boundary lookup URL for the service account. - * This implementation uses the configured service account email to construct - * the lookup endpoint. - * - * @return The regional access boundary URL string. - * @internal - */ - public async getRegionalAccessBoundaryUrl(): Promise { - if (!this.email) { - throw new Error( - 'RegionalAccessBoundary: An email address is required for regional access boundary lookups but was not provided in the JwtClient options.', - ); - } - const regionalAccessBoundaryUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(this.email), - ); - return regionalAccessBoundaryUrl; - } } diff --git a/core/packages/google-auth-library-nodejs/src/auth/oauth2client.ts b/core/packages/google-auth-library-nodejs/src/auth/oauth2client.ts index 4bf698df95e..138e66c462b 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/oauth2client.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/oauth2client.ts @@ -378,11 +378,6 @@ export interface RefreshAccessTokenResponse { export interface RequestMetadataResponse { headers: Headers; res?: GaxiosResponse | null; - /** - * Whether the returned headers contain an ID token (OIDC) instead of an - * access token. ID tokens are out of scope for Regional Access Boundaries. - */ - isIDToken?: boolean; } export interface RequestMetadataCallback { @@ -906,7 +901,8 @@ export class OAuth2Client extends AuthClient { } private async getAccessTokenAsync(): Promise { - const shouldRefresh = !this.credentials.access_token || this.isExpired(); + const shouldRefresh = + !this.credentials.access_token || this.isTokenExpiring(); if (shouldRefresh) { if (!this.credentials.refresh_token) { if (this.refreshHandler) { @@ -942,10 +938,7 @@ export class OAuth2Client extends AuthClient { * { authorization: 'Bearer ' } */ async getRequestHeaders(url?: string | URL): Promise { - const {headers, isIDToken} = await this.getRequestMetadataAsync(url); - if (!isIDToken) { - this.applyRegionalAccessBoundary(headers, url); - } + const headers = (await this.getRequestMetadataAsync(url)).headers; return headers; } @@ -1125,23 +1118,17 @@ export class OAuth2Client extends AuthClient { opts: GaxiosOptions, reAuthRetried = false, ): Promise> { - const requestOpts = {...opts}; try { - const {headers, isIDToken} = await this.getRequestMetadataAsync(); - requestOpts.headers = Gaxios.mergeHeaders(requestOpts.headers); + const r = await this.getRequestMetadataAsync(); + opts.headers = Gaxios.mergeHeaders(opts.headers); - this.applyHeadersFromSource(requestOpts.headers, headers); + this.addUserProjectAndAuthHeaders(opts.headers, r.headers); if (this.apiKey) { - requestOpts.headers.set('X-Goog-Api-Key', this.apiKey); - } - - if (!isIDToken) { - // Id token flows are outside the scope of Regional Access Boundary. - this.applyRegionalAccessBoundary(requestOpts.headers, opts.url); + opts.headers.set('X-Goog-Api-Key', this.apiKey); } - return await this.transporter.request(requestOpts); + return await this.transporter.request(opts); } catch (e) { const res = (e as GaxiosError).response; if (res) { diff --git a/core/packages/google-auth-library-nodejs/src/auth/regionalaccessboundary.ts b/core/packages/google-auth-library-nodejs/src/auth/regionalaccessboundary.ts deleted file mode 100644 index c490e18b526..00000000000 --- a/core/packages/google-auth-library-nodejs/src/auth/regionalaccessboundary.ts +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {Gaxios, GaxiosOptions} from 'gaxios'; -import {log as makeLog} from 'google-logging-utils'; - -const log = makeLog('auth'); - -export const SERVICE_ACCOUNT_LOOKUP_ENDPOINT = - 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{service_account_email}/allowedLocations'; - -export const WORKLOAD_LOOKUP_ENDPOINT = - 'https://iamcredentials.googleapis.com/v1/projects/{project_id}/locations/global/workloadIdentityPools/{pool_id}/allowedLocations'; - -export const WORKFORCE_LOOKUP_ENDPOINT = - 'https://iamcredentials.googleapis.com/v1/locations/global/workforcePools/{pool_id}/allowedLocations'; - -/** - * RAB is considered valid for 6 hours. - */ -const RAB_TTL_MILLIS = 6 * 60 * 60 * 1000; - -/** - * Grace period before hard expiry to trigger a background refresh (1 hour). - */ -const RAB_SOFT_EXPIRY_GRACE_PERIOD_MILLIS = 1 * 60 * 60 * 1000; - -/** - * Initial cooldown period for RAB lookup failures (15 minutes). - */ -const RAB_INITIAL_COOLDOWN_MILLIS = 15 * 60 * 1000; - -/** - * Maximum cooldown period for RAB lookup failures. - * Set as 6 hours. - */ -const RAB_MAX_COOLDOWN_MILLIS = 6 * 60 * 60 * 1000; - -/** - * Holds regional access boundary related information like locations - * where the credentials can be used. - */ -export interface RegionalAccessBoundaryData { - /** - * The readable text format of the allowed regional access boundary locations. - */ - locations?: string[]; - - /** - * The encoded text format of allowed regional access boundary locations. - */ - encodedLocations: string; -} - -export interface RegionalAccessBoundaryManagerOptions { - transporter: Gaxios; - getLookupUrl: () => Promise; - isUniverseDomainDefault: () => boolean; -} - -export class RegionalAccessBoundaryManager { - private regionalAccessBoundary: RegionalAccessBoundaryData | null = null; - private regionalAccessBoundaryExpiry = 0; - private regionalAccessBoundaryRefreshPromise: Promise | null = null; - private regionalAccessBoundaryCooldownTime = 0; - private regionalAccessBoundaryCooldownBackoff = RAB_INITIAL_COOLDOWN_MILLIS; - private options: RegionalAccessBoundaryManagerOptions; - private lookupUrl: string | null | undefined = undefined; - - constructor(options: RegionalAccessBoundaryManagerOptions) { - this.options = options; - } - - /** - * @internal - */ - get data(): RegionalAccessBoundaryData | null { - return this.regionalAccessBoundary; - } - - /** - * @internal - */ - get cooldownTime(): number { - return this.regionalAccessBoundaryCooldownTime; - } - - /** - * Returns the encoded locations string if the RAB is active and valid. - * Also triggers a background refresh if needed. - * @param url Optional endpoint URL being accessed. If missing, assumed global. - * @param headers The headers of the current request. - */ - getRegionalAccessBoundaryHeader( - url: string | URL | undefined, - headers: Headers, - ): string | null { - if (!this.options.isUniverseDomainDefault()) { - return null; - } - - // Only attach/refresh for global endpoints - if (url && !this.isGlobalEndpoint(url)) { - return null; - } - - // Attempt to trigger refresh if we have a token. - const authHeader = headers.get('authorization'); - if (authHeader && authHeader.startsWith('Bearer ')) { - // authHeader.substring(7) as auth header is of type 'Bearer XYZ...' - this.maybeTriggerRegionalAccessBoundaryRefresh(authHeader.substring(7)); - } - - if ( - this.regionalAccessBoundary && - this.regionalAccessBoundary.encodedLocations && - Date.now() < this.regionalAccessBoundaryExpiry - ) { - return this.regionalAccessBoundary.encodedLocations; - } - return null; - } - - /** - * Checks if the given URL is a global endpoint (not regional). - * @param url The URL to check. - */ - private isGlobalEndpoint(url: string | URL): boolean { - try { - const hostname = - url instanceof URL ? url.hostname : new URL(url).hostname; - return ( - !hostname.endsWith('.rep.googleapis.com') && - !hostname.endsWith('.rep.sandbox.googleapis.com') - ); - } catch { - // If the URL is relative or malformed, assume it is global. - return true; - } - } - - /** - * Triggers an asynchronous regional access boundary refresh if needed. - * @param accessToken The access token to use for the lookup. - */ - private maybeTriggerRegionalAccessBoundaryRefresh(accessToken: string): void { - if (this.lookupUrl === null) { - return; - } - - if (this.regionalAccessBoundaryRefreshPromise) { - return; - } - - const now = Date.now(); - - // Check if in cooldown - if (now < this.regionalAccessBoundaryCooldownTime) { - return; - } - - // Check if expired or never fetched (using soft expiry grace period) - const softExpiryThreshold = - this.regionalAccessBoundaryExpiry - RAB_SOFT_EXPIRY_GRACE_PERIOD_MILLIS; - if (!this.regionalAccessBoundary || now >= softExpiryThreshold) { - this.regionalAccessBoundaryRefreshPromise = - this.backgroundRefreshRegionalAccessBoundary(accessToken); - } - } - - /** - * Performs the background refresh of the regional access boundary. - * @param accessToken The access token to use for the lookup. - */ - private async backgroundRefreshRegionalAccessBoundary( - accessToken: string, - ): Promise { - try { - const data = await this.fetchRegionalAccessBoundary(accessToken); - if (data) { - this.regionalAccessBoundary = data; - this.regionalAccessBoundaryExpiry = Date.now() + RAB_TTL_MILLIS; - // Reset cooldown on success. - this.regionalAccessBoundaryCooldownTime = 0; - this.regionalAccessBoundaryCooldownBackoff = - RAB_INITIAL_COOLDOWN_MILLIS; - } - } catch (error) { - // Non-retryable or all retries failed: enter cooldown, - // initially for 15 mins which doubles after each failed cooldown 'exit' attempt. - this.regionalAccessBoundaryCooldownTime = - Date.now() + this.regionalAccessBoundaryCooldownBackoff; - this.regionalAccessBoundaryCooldownBackoff = Math.min( - this.regionalAccessBoundaryCooldownBackoff * 2, - RAB_MAX_COOLDOWN_MILLIS, - ); - log.error( - 'RegionalAccessBoundary: Lookup failed. Entering cooldown.', - error, - ); - } finally { - this.regionalAccessBoundaryRefreshPromise = null; - } - } - - /** - * Internal method to fetch RAB data. - * Retries for retryable 5xx errors from the RAB lookup endpoint. - * Throws if response from lookup is malformed. - */ - private async fetchRegionalAccessBoundary( - accessToken?: string, - ): Promise { - if (this.lookupUrl === undefined) { - this.lookupUrl = await this.options.getLookupUrl(); - } - if (!this.lookupUrl) { - return null; - } - - if (!accessToken) { - throw new Error( - 'RegionalAccessBoundary: Error calling lookup endpoint without valid access token', - ); - } - - const headers = new Headers({ - authorization: 'Bearer ' + accessToken, - }); - - const opts: GaxiosOptions = { - retry: true, - retryConfig: { - retry: 9, // Approximately 1 minute with default exponential backoff - retryDelay: 100, - httpMethodsToRetry: ['GET'], - statusCodesToRetry: [ - [500, 500], - [502, 504], - ], - }, - headers, - url: this.lookupUrl, - }; - - const {data: regionalAccessBoundaryData} = - await this.options.transporter.request(opts); - - if (!regionalAccessBoundaryData?.encodedLocations) { - throw new Error( - 'RegionalAccessBoundary: Malformed response from lookup endpoint.', - ); - } - - return regionalAccessBoundaryData; - } -} diff --git a/core/packages/google-auth-library-nodejs/src/util.ts b/core/packages/google-auth-library-nodejs/src/util.ts index 0d9081c4c2d..238ab604b10 100644 --- a/core/packages/google-auth-library-nodejs/src/util.ts +++ b/core/packages/google-auth-library-nodejs/src/util.ts @@ -300,40 +300,3 @@ export function getWellKnownCertificateConfigFileLocation(): string { function _isWindows(): boolean { return os.platform().startsWith('win'); } - -/** - * Returns the workforce identity pool ID if it is determinable - * from the audience resource name. - * @param audience The audience used to determine the pool ID. - * @return The pool ID associated with the workforce identity pool, if - * this can be determined from the audience field. Otherwise, null is - * returned. - */ -export function getWorkforcePoolIdFromAudience( - audience: string, -): string | null { - // STS audience pattern: - // .../workforcePools/$WORKFORCE_POOL_ID/providers/... - return ( - audience.match(/\/workforcePools\/(?[^/]+)\/providers\//)?.groups - ?.poolId ?? null - ); -} - -/** - * Returns the workload identity pool ID if it is determinable - * from the audience resource name. - * @param audience The audience used to determine the pool ID. - * @return The pool ID associated with the workload identity pool, if - * this can be determined from the audience field. Otherwise, null is - * returned. - */ -export function getWorkloadPoolIdFromAudience(audience: string): string | null { - // STS audience pattern: - // .../workloadIdentityPools/POOL_ID/providers/... - return ( - audience.match( - /\/workloadIdentityPools\/(?[^/]+)\/providers\//, - )?.groups?.workloadPool ?? null - ); -} diff --git a/core/packages/google-auth-library-nodejs/test/test.authclient.ts b/core/packages/google-auth-library-nodejs/test/test.authclient.ts index 3cef93f9121..22b2528c649 100644 --- a/core/packages/google-auth-library-nodejs/test/test.authclient.ts +++ b/core/packages/google-auth-library-nodejs/test/test.authclient.ts @@ -13,6 +13,7 @@ // limitations under the License. import {strict as assert} from 'assert'; + import * as nock from 'nock'; import { Gaxios, @@ -22,16 +23,10 @@ import { GaxiosResponse, } from 'gaxios'; -import {AuthClient, Compute, PassThroughClient} from '../src'; +import {AuthClient, PassThroughClient} from '../src'; import {snakeToCamel} from '../src/util'; import {PRODUCT_NAME, USER_AGENT} from '../src/shared.cjs'; import * as logging from 'google-logging-utils'; -import {BASE_PATH, HOST_ADDRESS, HEADERS} from 'gcp-metadata'; -import sinon = require('sinon'); -import { - RegionalAccessBoundaryData, - SERVICE_ACCOUNT_LOOKUP_ENDPOINT, -} from '../src/auth/regionalaccessboundary'; // Fakes for the logger, to capture logs that would've happened. interface TestLog { @@ -59,17 +54,6 @@ class TestLogSink implements logging.DebugLogBackend { } describe('AuthClient', () => { - let sandbox: sinon.SinonSandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - nock.cleanAll(); - }); - it('should accept and normalize snake case options to camel case', () => { const expected = { project_id: 'my-projectId', @@ -392,270 +376,5 @@ describe('AuthClient', () => { }); }); }); - - describe('regional access boundaries', () => { - const MOCK_ACCESS_TOKEN = 'abc123'; - const SERVICE_ACCOUNT_EMAIL = 'service-account@example.com'; - const EXPECTED_RAB_DATA: RegionalAccessBoundaryData = { - locations: ['us-central1', 'europe-west1'], - encodedLocations: '0x123', - }; - - function setupTokenNock( - email: string | 'default' = 'default', - ): nock.Scope { - const tokenPath = - email === 'default' - ? `${BASE_PATH}/instance/service-accounts/default/token` - : `${BASE_PATH}/instance/service-accounts/${email}/token`; - return nock(HOST_ADDRESS) - .get(tokenPath) - .reply( - 200, - {access_token: MOCK_ACCESS_TOKEN, expires_in: 10000}, - HEADERS, - ); - } - - it('should trigger asynchronous background refresh and not block', async () => { - const compute = new Compute({ - serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, - }); - // Set up nocks - const tokenScope = setupTokenNock(SERVICE_ACCOUNT_EMAIL); - // Use a promise to track when the RAB lookup is actually called - let rabLookupCalled = false; - const rabUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(SERVICE_ACCOUNT_EMAIL), - ); - - const rabScope = nock(new URL(rabUrl).origin) - .get(new URL(rabUrl).pathname) - .reply(() => { - rabLookupCalled = true; - return [200, EXPECTED_RAB_DATA]; - }); - - // Initial call - should NOT have the header yet because refresh is async - const headers = await compute.getRequestHeaders( - 'https://pubsub.googleapis.com', - ); - - assert.strictEqual(headers.get('x-allowed-locations'), null); - - // Wait for the background task to complete (not ideal but necessary for testing side effect) - // In a real scenario we'd use a better way to wait for the internal promise - let attempts = 0; - while (!rabLookupCalled && attempts < 10) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - - assert.strictEqual(rabLookupCalled, true); - - // Give the background processing a moment to update the class member - await new Promise(r => setTimeout(r, 50)); - assert.deepStrictEqual( - compute.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - tokenScope.done(); - rabScope.done(); - }); - - it('should NOT trigger lookup for regional endpoints', async () => { - const compute = new Compute({ - serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, - }); - - const tokenScope = setupTokenNock(SERVICE_ACCOUNT_EMAIL); - // No RAB nock setup here. If it's called, nock will throw. - - await compute.getRequestHeaders('https://us-east1.rep.googleapis.com'); - - tokenScope.done(); - // Assert no RAB lookup was attempted (implicitly verified by lack of nock error) - }); - - it('should NOT trigger lookup for non-GDU universes', async () => { - const compute = new Compute({ - serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, - universe_domain: 'custom-universe.com', - }); - - const tokenScope = setupTokenNock(SERVICE_ACCOUNT_EMAIL); - - await compute.getRequestHeaders('https://pubsub.googleapis.com'); - - tokenScope.done(); - // Assert no RAB lookup was attempted - }); - - it('should NOT crash and should trigger lookup for relative URLs', async () => { - const compute = new Compute({ - serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, - }); - - const tokenScope = setupTokenNock(SERVICE_ACCOUNT_EMAIL); - // If it treats the relative URL as global, it should try to call the RAB endpoint. - const rabUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{universe_domain}', - 'googleapis.com', - ).replace( - '{service_account_email}', - encodeURIComponent(SERVICE_ACCOUNT_EMAIL), - ); - - const rabScope = nock(new URL(rabUrl).origin) - .get(new URL(rabUrl).pathname) - .reply(200, EXPECTED_RAB_DATA); - - // This should NOT throw even though '/v1/resource' is relative - await compute.getRequestHeaders('/v1/resource'); - - // Wait for the background task to complete - let attempts = 0; - while (!compute.getRegionalAccessBoundary() && attempts < 10) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - - assert.deepStrictEqual( - compute.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - tokenScope.done(); - rabScope.done(); - }); - - it('should retry on retryable errors in background', async () => { - const compute = new Compute({ - serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, - }); - - setupTokenNock(SERVICE_ACCOUNT_EMAIL); - - // Mock 503 then 200 - const rabUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(SERVICE_ACCOUNT_EMAIL), - ); - - const rabFail = nock(new URL(rabUrl).origin) - .get(new URL(rabUrl).pathname) - .reply(503); - const rabSuccess = nock(new URL(rabUrl).origin) - .get(new URL(rabUrl).pathname) - .reply(200, EXPECTED_RAB_DATA); - - await compute.getRequestHeaders('https://pubsub.googleapis.com'); - - // Wait for retries (exponential backoff might take a moment) - let attempts = 0; - while (!compute.getRegionalAccessBoundary() && attempts < 20) { - await new Promise(r => setTimeout(r, 150)); - attempts++; - } - - assert.deepStrictEqual( - compute.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - rabFail.done(); - rabSuccess.done(); - }); - - it('should enter cooldown on non-retryable error', async () => { - const compute = new Compute({ - serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, - }); - - setupTokenNock(SERVICE_ACCOUNT_EMAIL); - - const rabUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(SERVICE_ACCOUNT_EMAIL), - ); - - const rabFail = nock(new URL(rabUrl).origin) - .get(new URL(rabUrl).pathname) - .reply(400, {error: 'Permanent failure'}); - - await compute.getRequestHeaders('https://pubsub.googleapis.com'); - - // Wait for it to fail and enter cooldown - let attempts = 0; - while ( - !compute.getRegionalAccessBoundaryCooldownTime() && - attempts < 10 - ) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - - assert.ok(compute.getRegionalAccessBoundaryCooldownTime() > Date.now()); - - // Subsequent call should NOT trigger nock (which would fail as we only set up 1) - await compute.getRequestHeaders('https://pubsub.googleapis.com'); - - rabFail.done(); - }); - - it('should only call getLookupUrl once and cache the null result if RAB is not supported', async () => { - const compute = new Compute({ - serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, - }); - - const tokenScope = setupTokenNock(SERVICE_ACCOUNT_EMAIL); - const spy = sandbox.spy(compute, 'getRegionalAccessBoundaryUrl'); - sandbox - .stub(compute as any, 'resolveServiceAccountEmail') - .resolves(null); - - // Make first request. This triggers the background refresh which calls getRegionalAccessBoundaryUrl once. - await compute.getRequestHeaders('https://pubsub.googleapis.com'); - - // Wait a short moment to ensure the async background refresh has finished. - await new Promise(r => setTimeout(r, 50)); - - assert.strictEqual(spy.callCount, 1); - - // Make second request. This should NOT trigger background refresh or call getRegionalAccessBoundaryUrl again. - await compute.getRequestHeaders('https://pubsub.googleapis.com'); - await new Promise(r => setTimeout(r, 50)); - - assert.strictEqual(spy.callCount, 1); - - tokenScope.done(); - }); - - it('should throw malformed response error if the response data is null', async () => { - const compute = new Compute({ - serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, - }); - - const rabUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(SERVICE_ACCOUNT_EMAIL), - ); - - // Reply with a 200 OK but null body - const rabNull = nock(new URL(rabUrl).origin) - .get(new URL(rabUrl).pathname) - .reply(200, null as any); - - const manager = (compute as any).regionalAccessBoundaryManager; - - await assert.rejects( - manager.fetchRegionalAccessBoundary('some-token'), - /RegionalAccessBoundary: Malformed response from lookup endpoint\./, - ); - - rabNull.done(); - }); - }); }); }); diff --git a/core/packages/google-auth-library-nodejs/test/test.baseexternalclient.ts b/core/packages/google-auth-library-nodejs/test/test.baseexternalclient.ts index 112da7c6016..3020b47cd49 100644 --- a/core/packages/google-auth-library-nodejs/test/test.baseexternalclient.ts +++ b/core/packages/google-auth-library-nodejs/test/test.baseexternalclient.ts @@ -38,16 +38,9 @@ import { mockGenerateAccessToken, mockStsTokenExchange, getExpectedExternalAccountMetricsHeaderValue, - saEmail, } from './externalclienthelper'; import {DEFAULT_UNIVERSE} from '../src/auth/authclient'; import {TestUtils} from './utils'; -import { - RegionalAccessBoundaryData, - SERVICE_ACCOUNT_LOOKUP_ENDPOINT, - WORKFORCE_LOOKUP_ENDPOINT, - WORKLOAD_LOOKUP_ENDPOINT, -} from '../src/auth/regionalaccessboundary'; nock.disableNetConnect(); interface SampleResponse { @@ -162,20 +155,11 @@ describe('BaseExternalAccountClient', () => { '//iam.googleapis.com/projects_suffix/123456', ]; - let sandbox: sinon.SinonSandbox; - beforeEach(() => { - sandbox = sinon.createSandbox(); - sandbox - .stub(BaseExternalAccountClient.prototype, 'getRegionalAccessBoundaryUrl') - .resolves(undefined); - }); - afterEach(() => { nock.cleanAll(); if (clock) { clock.restore(); } - sandbox.restore(); }); describe('Constructor', () => { @@ -2722,234 +2706,4 @@ describe('BaseExternalAccountClient', () => { ); }); }); - - describe('regional access boundaries', () => { - const MOCK_ACCESS_TOKEN = 'ACCESS_TOKEN'; - const MOCK_AUTH_HEADER = `Bearer ${MOCK_ACCESS_TOKEN}`; - const EXPECTED_RAB_DATA: RegionalAccessBoundaryData = { - locations: ['some-locations'], - encodedLocations: '0xdeadbeef', - }; - - beforeEach(() => { - ( - BaseExternalAccountClient.prototype - .getRegionalAccessBoundaryUrl as sinon.SinonStub - ).restore(); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - it('should trigger asynchronous RAB refresh for workload identity', async () => { - const projectNumber = '12345'; - const workloadPoolId = 'my-pool'; - const workloadAudience = `//iam.googleapis.com/projects/${projectNumber}/locations/global/workloadIdentityPools/${workloadPoolId}/providers/my-provider`; - const workloadOptions = { - ...externalAccountOptions, - audience: workloadAudience, - }; - const client = new TestExternalAccountClient(workloadOptions); - - const stsScope = mockStsTokenExchange([ - { - statusCode: 200, - response: {...stsSuccessfulResponse, access_token: MOCK_ACCESS_TOKEN}, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience: workloadAudience, - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token_0', - subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', - }, - }, - ]); - - const lookupUrl = WORKLOAD_LOOKUP_ENDPOINT.replace( - '{project_id}', - projectNumber, - ).replace('{pool_id}', workloadPoolId); - - let rabLookupCalled = false; - const rabScope = nock(new URL(lookupUrl).origin) - .get(new URL(lookupUrl).pathname) - .matchHeader('authorization', MOCK_AUTH_HEADER) - .reply(() => { - rabLookupCalled = true; - return [200, EXPECTED_RAB_DATA]; - }); - - // Initial call - should NOT have the header yet - const headers = await client.getRequestHeaders(); - assert.strictEqual(headers.get('x-allowed-locations'), null); - - // Wait for background lookup - let attempts = 0; - while (!rabLookupCalled && attempts < 10) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - assert.strictEqual(rabLookupCalled, true); - - await new Promise(r => setTimeout(r, 50)); - assert.deepStrictEqual( - client.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - stsScope.done(); - rabScope.done(); - }); - - it('should trigger asynchronous RAB refresh for workforce identity', async () => { - const workforcePoolId = 'my-workforce-pool'; - const location = 'global'; - const workforceAudience = `//iam.googleapis.com/locations/${location}/workforcePools/${workforcePoolId}/providers/my-provider`; - const workforceOptions = { - ...externalAccountOptions, - audience: workforceAudience, - }; - const client = new TestExternalAccountClient(workforceOptions); - - const stsScope = mockStsTokenExchange([ - { - statusCode: 200, - response: {...stsSuccessfulResponse, access_token: MOCK_ACCESS_TOKEN}, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience: workforceAudience, - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token_0', - subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', - }, - }, - ]); - - const lookupUrl = WORKFORCE_LOOKUP_ENDPOINT.replace( - '{location}', - location, - ).replace('{pool_id}', workforcePoolId); - - let rabLookupCalled = false; - const rabScope = nock(new URL(lookupUrl).origin) - .get(new URL(lookupUrl).pathname) - .matchHeader('authorization', MOCK_AUTH_HEADER) - .reply(() => { - rabLookupCalled = true; - return [200, EXPECTED_RAB_DATA]; - }); - - const headers = await client.getRequestHeaders(); - assert.strictEqual(headers.get('x-allowed-locations'), null); - - let attempts = 0; - while (!rabLookupCalled && attempts < 10) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - assert.strictEqual(rabLookupCalled, true); - - await new Promise(r => setTimeout(r, 50)); - assert.deepStrictEqual( - client.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - stsScope.done(); - rabScope.done(); - }); - - it('should fail background lookup for an invalid audience', async () => { - const invalidAudience = 'invalid-audience-format/providers/1235'; - const invalidOptions = { - ...externalAccountOptions, - audience: invalidAudience, - }; - const client = new TestExternalAccountClient(invalidOptions); - - // Note: background refresh fails silently in terms of getRequestHeaders resolving. - // But we can manually trigger getRegionalAccessBoundaryUrl to verify it throws. - await assert.rejects( - client.getRegionalAccessBoundaryUrl(), - /RegionalAccessBoundary: Invalid audience provided/, - ); - }); - - it('should trigger asynchronous RAB refresh for impersonated service account', async () => { - const projectNumber = '12345'; - const workloadPoolId = 'my-pool'; - const workloadAudience = `//iam.googleapis.com/projects/${projectNumber}/locations/global/workloadIdentityPools/${workloadPoolId}/providers/my-provider`; - const workloadOptions = { - ...externalAccountOptionsWithSA, - audience: workloadAudience, - }; - const client = new TestExternalAccountClient(workloadOptions); - - const stsScope = mockStsTokenExchange([ - { - statusCode: 200, - response: {...stsSuccessfulResponse, access_token: MOCK_ACCESS_TOKEN}, - request: { - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - audience: workloadAudience, - scope: 'https://www.googleapis.com/auth/cloud-platform', - requested_token_type: - 'urn:ietf:params:oauth:token-type:access_token', - subject_token: 'subject_token_0', - subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', - }, - }, - ]); - - const saSuccessResponse = { - accessToken: 'SA_ACCESS_TOKEN', - expireTime: new Date(Date.now() + 60 * 60 * 100).toISOString(), - }; - const impersonatedScope = mockGenerateAccessToken({ - statusCode: 200, - response: saSuccessResponse, - token: stsSuccessfulResponse.access_token, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - }); - - const lookupUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(saEmail), - ); - - let rabLookupCalled = false; - const rabScope = nock(new URL(lookupUrl).origin) - .get(new URL(lookupUrl).pathname) - .matchHeader('authorization', `Bearer ${saSuccessResponse.accessToken}`) - .reply(() => { - rabLookupCalled = true; - return [200, EXPECTED_RAB_DATA]; - }); - - const headers = await client.getRequestHeaders(); - assert.strictEqual(headers.get('x-allowed-locations'), null); - - let attempts = 0; - while (!rabLookupCalled && attempts < 10) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - assert.strictEqual(rabLookupCalled, true); - - await new Promise(r => setTimeout(r, 50)); - assert.deepStrictEqual( - client.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - stsScope.done(); - rabScope.done(); - impersonatedScope.done(); - }); - }); }); diff --git a/core/packages/google-auth-library-nodejs/test/test.compute.ts b/core/packages/google-auth-library-nodejs/test/test.compute.ts index 85f4367a516..981eac6c4f6 100644 --- a/core/packages/google-auth-library-nodejs/test/test.compute.ts +++ b/core/packages/google-auth-library-nodejs/test/test.compute.ts @@ -17,11 +17,7 @@ import {describe, it, beforeEach, afterEach} from 'mocha'; import {BASE_PATH, HEADERS, HOST_ADDRESS} from 'gcp-metadata'; import * as nock from 'nock'; import * as sinon from 'sinon'; -import {Compute, gcpMetadata} from '../src'; -import { - SERVICE_ACCOUNT_LOOKUP_ENDPOINT, - RegionalAccessBoundaryData, -} from '../src/auth/regionalaccessboundary'; +import {Compute} from '../src'; nock.disableNetConnect(); @@ -48,9 +44,6 @@ describe('compute', () => { let compute: Compute; beforeEach(() => { compute = new Compute(); - sandbox - .stub(Compute.prototype, 'getRegionalAccessBoundaryUrl') - .resolves(undefined); }); afterEach(() => { @@ -268,194 +261,4 @@ describe('compute', () => { assert.fail('failed to throw'); }); - describe('regional access boundaries', () => { - const MOCK_ACCESS_TOKEN = 'abc123'; - const MOCK_AUTH_HEADER = `Bearer ${MOCK_ACCESS_TOKEN}`; - const EXPECTED_RAB_DATA: RegionalAccessBoundaryData = { - locations: ['sadad', 'asdad'], - encodedLocations: '000x9', - }; - - function setupTokenNock(email: string | 'default' = 'default'): nock.Scope { - const tokenPath = - email === 'default' - ? `${BASE_PATH}/instance/service-accounts/default/token` - : `${BASE_PATH}/instance/service-accounts/${email}/token`; - return nock(HOST_ADDRESS) - .get(tokenPath) - .reply( - 200, - {access_token: MOCK_ACCESS_TOKEN, expires_in: 10000}, - HEADERS, - ); - } - - function setupRegionalAccessBoundaryNock( - email: string, - regionalAccessBoundaryData: RegionalAccessBoundaryData = EXPECTED_RAB_DATA, - ): nock.Scope { - const lookupUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(email), - ); - return nock(new URL(lookupUrl).origin) - .get(new URL(lookupUrl).pathname) - .matchHeader('authorization', MOCK_AUTH_HEADER) - .reply(200, regionalAccessBoundaryData); - } - - beforeEach(() => { - ( - Compute.prototype.getRegionalAccessBoundaryUrl as sinon.SinonStub - ).restore(); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - it('should trigger asynchronous RAB refresh using email from metadata server', async () => { - const compute = new Compute(); - const fakeEmail = 'fake-default-sa@developer.gserviceaccount.com'; - const metadataStub = sandbox.stub(gcpMetadata, 'instance'); - metadataStub.callThrough(); - metadataStub - .withArgs('service-accounts/default/email') - .resolves(fakeEmail); - - const tokenScope = setupTokenNock('default'); - const rabScope = setupRegionalAccessBoundaryNock(fakeEmail); - let rabLookupCalled = false; - rabScope.on('request', () => { - rabLookupCalled = true; - }); - - const url = 'https://pubsub.googleapis.com'; - const headers = await compute.getRequestHeaders(url); - - // Initial headers should NOT have RAB - assert.strictEqual(headers.get('x-allowed-locations'), null); - - // Wait for background tasks (email resolution + RAB lookup) - let attempts = 0; - while (!rabLookupCalled && attempts < 10) { - await new Promise(r => setTimeout(r, 100)); - attempts++; - } - assert.strictEqual(rabLookupCalled, true); - - await new Promise(r => setTimeout(r, 50)); - assert.deepStrictEqual( - compute.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - tokenScope.done(); - rabScope.done(); - }); - - it('should fail getRegionalAccessBoundaryUrl in background if metadata call fails', async () => { - const compute = new Compute(); - - const metadataStub = sandbox.stub(gcpMetadata, 'instance'); - metadataStub.callThrough(); - metadataStub - .withArgs('service-accounts/default/email') - .rejects(new Error('metadata failure')); - - // Error happens in background, so getRequestHeaders resolves fine. - // We manually call getRegionalAccessBoundaryUrl to verify the failure logic. - await assert.rejects( - compute.getRegionalAccessBoundaryUrl(), - /RegionalAccessBoundary: Failed to retrieve default service account email from metadata server./, - ); - }); - - it('should return null from getRegionalAccessBoundaryUrl if email returned from metadata server is not a valid email format', async () => { - const compute = new Compute(); - const fakeEmail = 'not-a-valid-email'; - const metadataStub = sandbox.stub(gcpMetadata, 'instance'); - metadataStub.callThrough(); - metadataStub - .withArgs('service-accounts/default/email') - .resolves(fakeEmail); - - const url = await compute.getRegionalAccessBoundaryUrl(); - assert.strictEqual(url, null); - }); - - it('should return valid URL from getRegionalAccessBoundaryUrl if custom serviceAccountEmail is set', async () => { - const email = 'custom-sa@example.com'; - const compute = new Compute({serviceAccountEmail: email}); - const url = await compute.getRegionalAccessBoundaryUrl(); - const expectedUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(email), - ); - assert.strictEqual(url, expectedUrl); - }); - - it('should return valid URL from getRegionalAccessBoundaryUrl when MDS returns a valid default service account email', async () => { - const compute = new Compute(); - const fakeEmail = 'fake-default-sa@developer.gserviceaccount.com'; - const metadataStub = sandbox.stub(gcpMetadata, 'instance'); - metadataStub.callThrough(); - metadataStub - .withArgs('service-accounts/default/email') - .resolves(fakeEmail); - - const url = await compute.getRegionalAccessBoundaryUrl(); - const expectedUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(fakeEmail), - ); - assert.strictEqual(url, expectedUrl); - }); - - it('should NOT trigger asynchronous RAB refresh and NOT attach RAB header if email from metadata server is not a valid email format', async () => { - const compute = new Compute(); - const fakeEmail = 'not-a-valid-email'; - const metadataStub = sandbox.stub(gcpMetadata, 'instance'); - metadataStub.callThrough(); - metadataStub - .withArgs('service-accounts/default/email') - .resolves(fakeEmail); - - const tokenScope = setupTokenNock('default'); - - const url = 'https://pubsub.googleapis.com'; - const headers = await compute.getRequestHeaders(url); - - // Headers should NOT have RAB - assert.strictEqual(headers.get('x-allowed-locations'), null); - - // Wait a little bit for any background task to run - await new Promise(r => setTimeout(r, 500)); - - // Regional access boundary data should remain null - assert.strictEqual(compute.getRegionalAccessBoundary(), null); - - tokenScope.done(); - }); - - it('should cache the service account email and avoid repeated metadata server calls when email is invalid', async () => { - const compute = new Compute(); - const fakeEmail = 'not-a-valid-email'; - const metadataStub = sandbox.stub(gcpMetadata, 'instance'); - metadataStub.callThrough(); - metadataStub - .withArgs('service-accounts/default/email') - .resolves(fakeEmail); - - // Call it the first time - let url = await compute.getRegionalAccessBoundaryUrl(); - assert.strictEqual(url, null); - assert.strictEqual(metadataStub.callCount, 1); - - // Call it a second time - should use cache and not call MDS again - url = await compute.getRegionalAccessBoundaryUrl(); - assert.strictEqual(url, null); - assert.strictEqual(metadataStub.callCount, 1); - }); - }); }); diff --git a/core/packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts b/core/packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts index ee46da87740..e5aca190ebc 100644 --- a/core/packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts +++ b/core/packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts @@ -17,7 +17,7 @@ import {describe, it, afterEach, beforeEach} from 'mocha'; import * as nock from 'nock'; import * as sinon from 'sinon'; import * as qs from 'querystring'; -import {assertGaxiosResponsePresent} from './externalclienthelper'; +import {assertGaxiosResponsePresent, getAudience} from './externalclienthelper'; import { EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, ExternalAccountAuthorizedUserClient, @@ -31,10 +31,6 @@ import { } from '../src/auth/oauth2common'; import {DEFAULT_UNIVERSE} from '../src/auth/authclient'; import {TestUtils} from './utils'; -import { - RegionalAccessBoundaryData, - WORKFORCE_LOOKUP_ENDPOINT, -} from '../src/auth/regionalaccessboundary'; nock.disableNetConnect(); @@ -87,11 +83,10 @@ describe('ExternalAccountAuthorizedUserClient', () => { let clock: sinon.SinonFakeTimers; const referenceDate = new Date('2020-08-11T06:55:22.345Z'); - const workforcePoolAudience = - '//iam.googleapis.com/locations/global/workforcePools/pool-id-123/providers/provider-id-abc'; + const audience = getAudience(); const externalAccountAuthorizedUserCredentialOptions = { type: EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, - audience: workforcePoolAudience, + audience: audience, client_id: 'clientId', client_secret: 'clientSecret', refresh_token: 'refreshToken', @@ -100,7 +95,7 @@ describe('ExternalAccountAuthorizedUserClient', () => { } as ExternalAccountAuthorizedUserClientOptions; const externalAccountAuthorizedUserCredentialOptionsNoToken = { type: EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE, - audience: workforcePoolAudience, + audience: audience, client_id: 'clientId', client_secret: 'clientSecret', refresh_token: 'refreshToken', @@ -898,90 +893,4 @@ describe('ExternalAccountAuthorizedUserClient', () => { scopes.forEach(scope => scope.done()); }); }); - - describe('regional access boundaries', () => { - const MOCK_ACCESS_TOKEN = 'newAccessToken'; - const MOCK_AUTH_HEADER = `Bearer ${MOCK_ACCESS_TOKEN}`; - const EXPECTED_RAB_DATA: RegionalAccessBoundaryData = { - locations: ['some-locations'], - encodedLocations: '0xdeadbeef', - }; - - beforeEach(() => { - clock.restore(); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - it('should trigger asynchronous RAB refresh successfully', async () => { - const workforcePoolId = 'pool-id-123'; - const client = new ExternalAccountAuthorizedUserClient( - externalAccountAuthorizedUserCredentialOptions, - ); - - const stsScope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ - { - statusCode: 200, - response: successfulRefreshResponse, - request: { - grant_type: 'refresh_token', - refresh_token: 'refreshToken', - }, - }, - ]); - - const lookupUrl = WORKFORCE_LOOKUP_ENDPOINT.replace( - '{pool_id}', - encodeURIComponent(workforcePoolId), - ); - - let rabLookupCalled = false; - const rabScope = nock(new URL(lookupUrl).origin) - .get(new URL(lookupUrl).pathname) - .matchHeader('authorization', MOCK_AUTH_HEADER) - .reply(() => { - rabLookupCalled = true; - return [200, EXPECTED_RAB_DATA]; - }); - - // Initial call - should NOT have the header yet - const headers = await client.getRequestHeaders(); - assert.strictEqual(headers.get('x-allowed-locations'), null); - - // Wait for background lookup - let attempts = 0; - while (!rabLookupCalled && attempts < 20) { - await new Promise(r => setTimeout(r, 100)); - attempts++; - } - assert.strictEqual(rabLookupCalled, true); - - await new Promise(r => setTimeout(r, 100)); - assert.deepStrictEqual( - client.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - stsScope.done(); - rabScope.done(); - }); - - it('should fail background lookup for an invalid audience', async () => { - const invalidAudience = 'invalid-audience-format'; - const options = { - ...externalAccountAuthorizedUserCredentialOptions, - audience: invalidAudience, - }; - const client = new ExternalAccountAuthorizedUserClient(options); - - // Note: background refresh fails silently in terms of getRequestHeaders resolving. - // But we can manually trigger getRegionalAccessBoundaryUrl to verify it throws. - await assert.rejects( - client.getRegionalAccessBoundaryUrl(), - /RegionalAccessBoundary: A workforce pool ID is required for regional access boundary lookups but could not be determined from the audience/, - ); - }); - }); }); diff --git a/core/packages/google-auth-library-nodejs/test/test.externalclient.ts b/core/packages/google-auth-library-nodejs/test/test.externalclient.ts index 6c64d360150..d85a420b7fc 100644 --- a/core/packages/google-auth-library-nodejs/test/test.externalclient.ts +++ b/core/packages/google-auth-library-nodejs/test/test.externalclient.ts @@ -109,35 +109,45 @@ describe('ExternalAccountClient', () => { ]; it('should return IdentityPoolClient on IdentityPoolClientOptions', () => { - const client = ExternalAccountClient.fromJSON(fileSourcedOptions); - assert.ok(client instanceof IdentityPoolClient); + const expectedClient = new IdentityPoolClient(fileSourcedOptions); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(fileSourcedOptions), + expectedClient, + ); }); it('should return IdentityPoolClient with expected RefreshOptions', () => { - const client = ExternalAccountClient.fromJSON({ + const expectedClient = new IdentityPoolClient({ ...fileSourcedOptions, ...refreshOptions, }); - assert.ok(client instanceof IdentityPoolClient); - assert.strictEqual(client!.eagerRefreshThresholdMillis, 10000); - assert.strictEqual(client!.forceRefreshOnFailure, true); + assert.deepStrictEqual( + ExternalAccountClient.fromJSON({ + ...fileSourcedOptions, + ...refreshOptions, + }), + expectedClient, + ); }); it('should return AwsClient on AwsClientOptions', () => { - const client = ExternalAccountClient.fromJSON(awsOptions); - assert.ok(client instanceof AwsClient); + const expectedClient = new AwsClient(awsOptions); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(awsOptions), + expectedClient, + ); }); it('should return AwsClient with expected RefreshOptions', () => { - const client = ExternalAccountClient.fromJSON({ - ...awsOptions, - ...refreshOptions, - }); + const expectedClient = new AwsClient({...awsOptions, ...refreshOptions}); - assert.ok(client instanceof AwsClient); - assert.strictEqual(client!.eagerRefreshThresholdMillis, 10000); - assert.strictEqual(client!.forceRefreshOnFailure, true); + assert.deepStrictEqual( + ExternalAccountClient.fromJSON({...awsOptions, ...refreshOptions}), + expectedClient, + ); }); it('should return an IdentityPoolClient with a workforce config', () => { @@ -157,28 +167,41 @@ describe('ExternalAccountClient', () => { for (const validWorkforceIdentityPoolClientAudience of validWorkforceIdentityPoolClientAudiences) { workforceFileSourcedOptions.audience = validWorkforceIdentityPoolClientAudience; - - const client = ExternalAccountClient.fromJSON( + const expectedClient = new IdentityPoolClient( workforceFileSourcedOptions, ); - assert.ok(client instanceof IdentityPoolClient); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(workforceFileSourcedOptions), + expectedClient, + ); } }); it('should return PluggableAuthClient on PluggableAuthClientOptions', () => { - const client = ExternalAccountClient.fromJSON(pluggableAuthClientOptions); - assert.ok(client instanceof PluggableAuthClient); + const expectedClient = new PluggableAuthClient( + pluggableAuthClientOptions, + ); + + assert.deepStrictEqual( + ExternalAccountClient.fromJSON(pluggableAuthClientOptions), + expectedClient, + ); }); it('should return PluggableAuthClient with expected RefreshOptions', () => { - const client = ExternalAccountClient.fromJSON({ + const expectedClient = new PluggableAuthClient({ ...pluggableAuthClientOptions, ...refreshOptions, }); - assert.ok(client instanceof PluggableAuthClient); - assert.strictEqual(client!.eagerRefreshThresholdMillis, 10000); - assert.strictEqual(client!.forceRefreshOnFailure, true); + assert.deepStrictEqual( + ExternalAccountClient.fromJSON({ + ...pluggableAuthClientOptions, + ...refreshOptions, + }), + expectedClient, + ); }); invalidWorkforceIdentityPoolClientAudiences.forEach( diff --git a/core/packages/google-auth-library-nodejs/test/test.impersonated.ts b/core/packages/google-auth-library-nodejs/test/test.impersonated.ts index e97d77c74a3..e7be5ddf177 100644 --- a/core/packages/google-auth-library-nodejs/test/test.impersonated.ts +++ b/core/packages/google-auth-library-nodejs/test/test.impersonated.ts @@ -19,11 +19,6 @@ import * as nock from 'nock'; import {describe, it, afterEach} from 'mocha'; import {Impersonated, JWT, UserRefreshClient} from '../src'; import {CredentialRequest} from '../src/auth/credentials'; -import { - SERVICE_ACCOUNT_LOOKUP_ENDPOINT, - RegionalAccessBoundaryData, -} from '../src/auth/regionalaccessboundary'; -import sinon = require('sinon'); const PEM_PATH = './test/fixtures/private.pem'; @@ -74,15 +69,8 @@ interface ImpersonatedCredentialRequest { } describe('impersonated', () => { - beforeEach(() => { - sinon - .stub(Impersonated.prototype, 'getRegionalAccessBoundaryUrl') - .resolves(undefined); - }); - afterEach(() => { nock.cleanAll(); - sinon.restore(); }); it('should request impersonated credentials on first request', async () => { @@ -601,107 +589,4 @@ describe('impersonated', () => { assert.equal(resp.signedBlob, expectedSignedBlob); scopes.forEach(s => s.done()); }); - - describe('regional access boundaries', () => { - const TARGET_PRINCIPAL_EMAIL = 'target@project.iam.gserviceaccount.com'; - const MOCK_ACCESS_TOKEN = 'abc123'; - const MOCK_AUTH_HEADER = `Bearer ${MOCK_ACCESS_TOKEN}`; - - const EXPECTED_RAB_DATA: RegionalAccessBoundaryData = { - locations: ['sadad', 'asdad'], - encodedLocations: '000x9', - }; - - function setupRegionalAccessBoundaryNock( - email: string, - regionalAccessBoundaryData: RegionalAccessBoundaryData = EXPECTED_RAB_DATA, - ): nock.Scope { - const lookupUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(email), - ); - return nock(new URL(lookupUrl).origin) - .get(new URL(lookupUrl).pathname) - .matchHeader('authorization', MOCK_AUTH_HEADER) - .reply(200, regionalAccessBoundaryData); - } - - beforeEach(() => { - ( - Impersonated.prototype.getRegionalAccessBoundaryUrl as sinon.SinonStub - ).restore(); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - it('should trigger asynchronous RAB refresh', async () => { - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); - const impersonated = new Impersonated({ - sourceClient: createSampleJWTClient(), - targetPrincipal: TARGET_PRINCIPAL_EMAIL, - lifetime: 30, - delegates: [], - targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], - }); - - const tokenScope = createGTokenMock({access_token: MOCK_ACCESS_TOKEN}); - const saScope = nock('https://iamcredentials.googleapis.com') - .post( - `/v1/projects/-/serviceAccounts/${TARGET_PRINCIPAL_EMAIL}:generateAccessToken`, - ) - .reply(200, { - accessToken: MOCK_ACCESS_TOKEN, - expireTime: tomorrow.toISOString(), - }); - - let rabLookupCalled = false; - const rabScope = setupRegionalAccessBoundaryNock(TARGET_PRINCIPAL_EMAIL); - rabScope.on('request', () => { - rabLookupCalled = true; - }); - - const url = 'https://pubsub.googleapis.com'; - const headers = await impersonated.getRequestHeaders(url); - - // Initial headers should NOT have RAB - assert.strictEqual(headers.get('x-allowed-locations'), null); - - // Wait for background lookup - let attempts = 0; - while (!rabLookupCalled && attempts < 10) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - assert.strictEqual(rabLookupCalled, true); - - await new Promise(r => setTimeout(r, 50)); - assert.deepStrictEqual( - impersonated.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - tokenScope.done(); - saScope.done(); - rabScope.done(); - }); - - it('should fail getRegionalAccessBoundaryUrl in background if no target principal is specified', async () => { - const impersonated = new Impersonated({ - sourceClient: createSampleJWTClient(), - // targetPrincipal missing - lifetime: 30, - delegates: [], - targetScopes: ['https://www.googleapis.com/auth/cloud-platform'], - }); - - // Error happens in background. - await assert.rejects( - impersonated.getRegionalAccessBoundaryUrl(), - /RegionalAccessBoundary: A targetPrincipal is required for regional access boundary lookups but was not provided in the ImpersonatedClient options./, - ); - }); - }); }); diff --git a/core/packages/google-auth-library-nodejs/test/test.jwt.ts b/core/packages/google-auth-library-nodejs/test/test.jwt.ts index d9c4f93cc3b..cb5ed85ba4c 100644 --- a/core/packages/google-auth-library-nodejs/test/test.jwt.ts +++ b/core/packages/google-auth-library-nodejs/test/test.jwt.ts @@ -22,10 +22,6 @@ import * as sinon from 'sinon'; import {GoogleAuth, JWT} from '../src'; import {CredentialRequest, JWTInput} from '../src/auth/credentials'; import * as jwtaccess from '../src/auth/jwtaccess'; -import { - SERVICE_ACCOUNT_LOOKUP_ENDPOINT, - RegionalAccessBoundaryData, -} from '../src/auth/regionalaccessboundary'; function removeBearerFromAuthorizationHeader(headers: Headers): string { return (headers.get('authorization') || '').replace('Bearer ', ''); @@ -72,9 +68,6 @@ describe('jwt', () => { json = createJSON(); jwt = new JWT(); sandbox = sinon.createSandbox(); - sandbox - .stub(JWT.prototype, 'getRegionalAccessBoundaryUrl') - .resolves(undefined); }); afterEach(() => { @@ -1251,169 +1244,4 @@ describe('jwt', () => { assert.strictEqual(headers.get('authorization'), want); }); }); - - describe('regional access boundaries', () => { - const SERVICE_ACCOUNT_EMAIL = 'service-account@example.com'; - const MOCK_ACCESS_TOKEN = 'abc123'; - const MOCK_AUTH_HEADER = `Bearer ${MOCK_ACCESS_TOKEN}`; - - const EXPECTED_RAB_DATA: RegionalAccessBoundaryData = { - locations: ['sadad', 'asdad'], - encodedLocations: '000x9', - }; - - function setupRegionalAccessBoundaryNock( - email: string, - regionalAccessBoundaryData: RegionalAccessBoundaryData = EXPECTED_RAB_DATA, - authHeader = MOCK_AUTH_HEADER, - ): nock.Scope { - const lookupUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(email), - ); - return nock(new URL(lookupUrl).origin) - .get(new URL(lookupUrl).pathname) - .matchHeader('authorization', authHeader) - .reply(200, regionalAccessBoundaryData); - } - - beforeEach(() => { - (JWT.prototype.getRegionalAccessBoundaryUrl as sinon.SinonStub).restore(); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - it('should trigger asynchronous regional access boundaries refresh', async () => { - const jwt = new JWT({ - email: SERVICE_ACCOUNT_EMAIL, - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - - const tokenScope = createGTokenMock({access_token: MOCK_ACCESS_TOKEN}); - - let rabLookupCalled = false; - const rabScope = setupRegionalAccessBoundaryNock(SERVICE_ACCOUNT_EMAIL); - rabScope.on('request', () => { - rabLookupCalled = true; - }); - - // Initial call - headers should NOT have the RAB yet - const headers = await jwt.getRequestHeaders( - 'https://pubsub.googleapis.com', - ); - assert.strictEqual(headers.get('x-allowed-locations'), null); - - // Wait for background lookup - let attempts = 0; - while (!rabLookupCalled && attempts < 10) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - assert.strictEqual(rabLookupCalled, true); - - // Give it a moment to update state - await new Promise(r => setTimeout(r, 50)); - assert.deepStrictEqual( - jwt.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - tokenScope.done(); - rabScope.done(); - }); - - it('should trigger RAB refresh for self-signed JWT', async () => { - // Self-signed JWT (no scopes) - const keys = keypair(512); - const jwt = new JWT({ - email: SERVICE_ACCOUNT_EMAIL, - key: keys.private, - }); - jwt.credentials = {refresh_token: 'jwt-placeholder'}; - - const lookupUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( - '{service_account_email}', - encodeURIComponent(SERVICE_ACCOUNT_EMAIL), - ); - - let rabLookupCalled = false; - // For self-signed JWT, the lookup uses the JWT itself as the token - const rabScope = nock(new URL(lookupUrl).origin) - .get(new URL(lookupUrl).pathname) - .reply(() => { - rabLookupCalled = true; - return [200, EXPECTED_RAB_DATA]; - }); - - const url = 'https://pubsub.googleapis.com'; - const headers = await jwt.getRequestHeaders(url); - - // Verify headers contain the self-signed JWT - const authHeader = headers.get('authorization'); - assert.ok(authHeader?.startsWith('Bearer ')); - - // Wait for background lookup - let attempts = 0; - while (!rabLookupCalled && attempts < 10) { - await new Promise(r => setTimeout(r, 50)); - attempts++; - } - assert.strictEqual(rabLookupCalled, true); - - await new Promise(r => setTimeout(r, 50)); - assert.deepStrictEqual( - jwt.getRegionalAccessBoundary(), - EXPECTED_RAB_DATA, - ); - - rabScope.done(); - }); - - it('should NOT add RAB headers for ID tokens', async () => { - const jwt = new JWT({ - email: SERVICE_ACCOUNT_EMAIL, - key: PEM_CONTENTS, - additionalClaims: {target_audience: 'some-audience'}, - }); - - // Setup a RAB lookup mock that should NOT be hit - const rabScope = setupRegionalAccessBoundaryNock(SERVICE_ACCOUNT_EMAIL); - - const scope = createGTokenMock({id_token: 'id-token-abc'}); - const headers = await jwt.getRequestHeaders( - 'https://pubsub.googleapis.com', - ); - - assert.strictEqual(headers.get('authorization'), 'Bearer id-token-abc'); - // Should NOT have the RAB header because it's an ID token - assert.strictEqual(headers.get('x-allowed-locations'), null); - - // Ensure RAB lookup was NOT called - assert.strictEqual(rabScope.isDone(), false); - - scope.done(); - }); - - it('should fail getRegionalAccessBoundaryUrl if no email is passed', async () => { - const jwt = new JWT({ - keyFile: PEM_PATH, - scopes: ['http://bar', 'http://foo'], - subject: 'bar@subjectaccount.com', - }); - // Ensure email is explicitly undefined - jwt.email = undefined; - - // Note: error happens in background during getRequestHeaders, - // but we can manually call getRegionalAccessBoundaryUrl to verify it throws. - await assert.rejects( - jwt.getRegionalAccessBoundaryUrl(), - /RegionalAccessBoundary: An email address is required for regional access boundary lookups but was not provided in the JwtClient options./, - ); - }); - }); }); diff --git a/core/packages/google-auth-library-nodejs/tsconfig.json b/core/packages/google-auth-library-nodejs/tsconfig.json index b9f7f940bae..572f3da7166 100644 --- a/core/packages/google-auth-library-nodejs/tsconfig.json +++ b/core/packages/google-auth-library-nodejs/tsconfig.json @@ -1,10 +1,7 @@ { "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { - "lib": [ - "es2023", - "DOM" - ], + "lib": ["DOM"], "composite": true, "rootDir": ".", "outDir": "build",