From 79651482c3482b09a6c448e8d1147f77b2d84f81 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 5 Jan 2024 11:09:10 -0800 Subject: [PATCH 1/7] feat: Allow custom STS Access Token URL for Downscoped Clients --- src/auth/downscopedclient.ts | 25 +++++++++++++++++++++--- src/auth/stscredentials.ts | 4 ++-- test/externalclienthelper.ts | 5 +++-- test/test.downscopedclient.ts | 36 +++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 92238b40..da06d87f 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -39,8 +39,11 @@ const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; * The requested token exchange subject_token_type: rfc8693#section-2.1 */ const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; -/** The STS access token exchange end point. */ -const STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1/token'; + +/** + * The default STS access token exchange endpoint. + **/ +const DEFAULT_STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1/token'; /** * The maximum number of access boundary rules a Credential Access Boundary @@ -75,6 +78,13 @@ export interface CredentialAccessBoundary { accessBoundary: { accessBoundaryRules: AccessBoundaryRule[]; }; + /** + * An optional STS access token exchange endpoint. + * + * @example + * 'https://sts.googleapis.com/v1/token' + */ + tokenURL?: string | URL; } /** Defines an upper bound of permissions on a particular resource. */ @@ -135,6 +145,12 @@ export class DownscopedClient extends AuthClient { quotaProjectId?: string ) { super({...additionalOptions, quotaProjectId}); + + // extract and remove `tokenURL` as it is not officially a part of the credentialAccessBoundary + this.credentialAccessBoundary = {...credentialAccessBoundary}; + const tokenURL = this.credentialAccessBoundary.tokenURL; + delete this.credentialAccessBoundary.tokenURL; + // Check 1-10 Access Boundary Rules are defined within Credential Access // Boundary. if ( @@ -162,7 +178,10 @@ export class DownscopedClient extends AuthClient { } } - this.stsCredential = new sts.StsCredentials(STS_ACCESS_TOKEN_URL); + this.stsCredential = new sts.StsCredentials( + tokenURL || DEFAULT_STS_ACCESS_TOKEN_URL + ); + this.cachedDownscopedAccessToken = null; } diff --git a/src/auth/stscredentials.ts b/src/auth/stscredentials.ts index 44f431b7..a075eae1 100644 --- a/src/auth/stscredentials.ts +++ b/src/auth/stscredentials.ts @@ -140,7 +140,7 @@ export class StsCredentials extends OAuthClientAuthHandler { * available. */ constructor( - private readonly tokenExchangeEndpoint: string, + private readonly tokenExchangeEndpoint: string | URL, clientAuthentication?: ClientAuthentication ) { super(clientAuthentication); @@ -195,7 +195,7 @@ export class StsCredentials extends OAuthClientAuthHandler { Object.assign(headers, additionalHeaders || {}); const opts: GaxiosOptions = { - url: this.tokenExchangeEndpoint, + url: this.tokenExchangeEndpoint.toString(), method: 'POST', headers, data: querystring.stringify( diff --git a/test/externalclienthelper.ts b/test/externalclienthelper.ts index 81d190ef..9cc62adf 100644 --- a/test/externalclienthelper.ts +++ b/test/externalclienthelper.ts @@ -61,7 +61,8 @@ const pkg = require('../../package.json'); export function mockStsTokenExchange( nockParams: NockMockStsToken[], - additionalHeaders?: {[key: string]: string} + additionalHeaders?: {[key: string]: string}, + baseURL = baseUrl ): nock.Scope { const headers = Object.assign( { @@ -69,7 +70,7 @@ export function mockStsTokenExchange( }, additionalHeaders || {} ); - const scope = nock(baseUrl, {reqheaders: headers}); + const scope = nock(baseURL, {reqheaders: headers}); nockParams.forEach(nockMockStsToken => { scope .post(path, qs.stringify(nockMockStsToken.request)) diff --git a/test/test.downscopedclient.ts b/test/test.downscopedclient.ts index d280a9ee..23baf404 100644 --- a/test/test.downscopedclient.ts +++ b/test/test.downscopedclient.ts @@ -325,6 +325,42 @@ describe('DownscopedClient', () => { refreshOptions.eagerRefreshThresholdMillis ); }); + + it('should use a tokenURL', async () => { + const tokenURL = new URL('https://my-token-url/v1/token'); + const scope = mockStsTokenExchange( + [ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + 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:access_token', + options: JSON.stringify(testClientAccessBoundary), + }, + }, + ], + undefined, + tokenURL.origin + ); + + const downscopedClient = new DownscopedClient(client, { + ...testClientAccessBoundary, + tokenURL, + }); + + const tokenResponse = await downscopedClient.getAccessToken(); + assert.deepStrictEqual( + tokenResponse.token, + stsSuccessfulResponse.access_token + ); + + scope.done(); + }); }); describe('setCredential()', () => { From 69c8f0598b2e60d83f61c11f73790d2bea80a2f9 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 5 Jan 2024 17:18:33 -0800 Subject: [PATCH 2/7] feat: Open More Endpoints for Customization --- src/auth/baseexternalclient.ts | 43 ++++---- src/auth/googleauth.ts | 20 ++-- src/auth/oauth2client.ts | 169 +++++++++++++++++++++----------- test/test.baseexternalclient.ts | 37 ------- test/test.externalclient.ts | 38 ------- test/test.identitypoolclient.ts | 38 ------- 6 files changed, 147 insertions(+), 198 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 6496b143..6d69ea79 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -54,11 +54,8 @@ export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000; */ export const EXTERNAL_ACCOUNT_TYPE = 'external_account'; /** Cloud resource manager URL used to retrieve project information. */ -export const CLOUD_RESOURCE_MANAGER = +export const DEFAULT_CLOUD_RESOURCE_MANAGER = 'https://cloudresourcemanager.googleapis.com/v1/projects/'; -/** The workforce audience pattern. */ -const WORKFORCE_AUDIENCE_PATTERN = - '//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/[^/]+/providers/.+'; // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); @@ -88,6 +85,12 @@ export interface BaseExternalAccountClientOptions client_id?: string; client_secret?: string; workforce_pool_user_project?: string; + scopes?: string[]; + /** + * @example + * https://cloudresourcemanager.googleapis.com/v1/projects/ + **/ + cloud_resource_manager_url?: string | URL; } /** @@ -150,6 +153,13 @@ export abstract class BaseExternalAccountClient extends AuthClient { public projectNumber: string | null; private readonly configLifetimeRequested: boolean; protected credentialSourceType?: string; + /** + * @example + * ```ts + * new URL('https://cloudresourcemanager.googleapis.com/v1/projects/'); + * ``` + */ + protected cloudResourceManagerURL = new URL(DEFAULT_CLOUD_RESOURCE_MANAGER); /** * Instantiate a BaseExternalAccountClient instance using the provided JSON * object loaded from an external account credentials file. @@ -195,6 +205,10 @@ export abstract class BaseExternalAccountClient extends AuthClient { serviceAccountImpersonation ).get('token_lifetime_seconds'); + this.cloudResourceManagerURL = new URL( + opts.get('cloud_resource_manager_url') || DEFAULT_CLOUD_RESOURCE_MANAGER + ); + if (clientId) { this.clientAuth = { confidentialClientType: 'basic', @@ -204,22 +218,11 @@ export abstract class BaseExternalAccountClient extends AuthClient { } this.stsCredential = new sts.StsCredentials(tokenUrl, this.clientAuth); - // Default OAuth scope. This could be overridden via public property. - this.scopes = [DEFAULT_OAUTH_SCOPE]; + this.scopes = opts.get('scopes') || [DEFAULT_OAUTH_SCOPE]; this.cachedAccessToken = null; this.audience = opts.get('audience'); this.subjectTokenType = subjectTokenType; this.workforcePoolUserProject = workforcePoolUserProject; - const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN); - if ( - this.workforcePoolUserProject && - !this.audience.match(workforceAudiencePattern) - ) { - throw new Error( - 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' - ); - } this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl; this.serviceAccountImpersonationLifetime = serviceAccountImpersonationLifetime; @@ -360,7 +363,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { const headers = await this.getRequestHeaders(); const response = await this.transporter.request({ headers, - url: `${CLOUD_RESOURCE_MANAGER}${projectNumber}`, + url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`, responseType: 'json', }); this.projectId = response.data.projectId; @@ -576,11 +579,9 @@ export abstract class BaseExternalAccountClient extends AuthClient { // be normalized. if (typeof this.scopes === 'string') { return [this.scopes]; - } else if (typeof this.scopes === 'undefined') { - return [DEFAULT_OAUTH_SCOPE]; - } else { - return this.scopes; } + + return this.scopes || [DEFAULT_OAUTH_SCOPE]; } private getMetricsHeaderValue(): string { diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index ed51e3dc..a5f78f74 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -1060,8 +1060,14 @@ export class GoogleAuth { * Sign the given data with the current private key, or go out * to the IAM API to sign it. * @param data The data to be signed. + * @param endpoint A custom endpoint to use. + * + * @example + * ``` + * sign('data', 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/'); + * ``` */ - async sign(data: string): Promise { + async sign(data: string, endpoint?: string): Promise { const client = await this.getClient(); if (client instanceof Impersonated) { @@ -1080,24 +1086,24 @@ export class GoogleAuth { throw new Error('Cannot sign data without `client_email`.'); } - return this.signBlob(crypto, creds.client_email, data); + return this.signBlob(crypto, creds.client_email, data, endpoint); } private async signBlob( crypto: Crypto, emailOrUniqueId: string, - data: string + data: string, + endpoint = 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' ): Promise { - const url = - 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' + - `${emailOrUniqueId}:signBlob`; + const url = new URL(endpoint + `${emailOrUniqueId}:signBlob`); const res = await this.request({ method: 'POST', - url, + url: url.href, data: { payload: crypto.encodeBase64StringUtf8(data), }, }); + return res.data.signedBlob; } } diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index c5ecf5b2..fe233d09 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -404,10 +404,77 @@ export interface VerifyIdTokenOptions { maxExpiry?: number; } +export interface OAuth2ClientEndpoints { + /** + * The + * + * @example + * 'https://oauth2.googleapis.com/tokeninfo' + */ + tokenInfoUrl: string | URL; + + /** + * The base URL for auth endpoints. + * + * @example + * 'https://accounts.google.com/o/oauth2/v2/auth' + */ + oauth2AuthBaseUrl: string | URL; + + /** + * The base endpoint for token retrieval + * . + * @example + * 'https://oauth2.googleapis.com/token' + */ + oauth2TokenUrl: string | URL; + + /** + * The base endpoint to revoke tokens. + * + * @example + * 'https://oauth2.googleapis.com/revoke' + */ + oauth2RevokeUrl: string | URL; + + /** + * Sign on certificates in PEM format. + * + * @example + * 'https://www.googleapis.com/oauth2/v1/certs' + */ + oauth2FederatedSignonPemCertsUrl: string | URL; + + /** + * Sign on certificates in JWK format. + * + * @example + * 'https://www.googleapis.com/oauth2/v3/certs' + */ + oauth2FederatedSignonJwkCertsUrl: string | URL; + + /** + * IAP Public Key URL. + * This URL contains a JSON dictionary that maps the `kid` claims to the public key values. + * + * @example + * 'https://www.gstatic.com/iap/verify/public_key' + */ + oauth2IapPublicKeyUrl: string | URL; +} + export interface OAuth2ClientOptions extends AuthClientOptions { clientId?: string; clientSecret?: string; redirectUri?: string; + /** + * Customizable endpoints. + */ + endpoints?: Partial; + /** + * The allowed OAuth2 token issuers. + */ + issuers?: string[]; } // Re-exporting here for backwards compatibility @@ -422,6 +489,8 @@ export class OAuth2Client extends AuthClient { private certificateExpiry: Date | null = null; private certificateCacheFormat: CertificateFormat = CertificateFormat.PEM; protected refreshTokenPromises = new Map>(); + readonly endpoints: Readonly; + readonly issuers: string[]; // TODO: refactor tests to make this private _clientId?: string; @@ -460,46 +529,31 @@ export class OAuth2Client extends AuthClient { this._clientId = opts.clientId; this._clientSecret = opts.clientSecret; this.redirectUri = opts.redirectUri; - } - - protected static readonly GOOGLE_TOKEN_INFO_URL = - 'https://oauth2.googleapis.com/tokeninfo'; - - /** - * The base URL for auth endpoints. - */ - private static readonly GOOGLE_OAUTH2_AUTH_BASE_URL_ = - 'https://accounts.google.com/o/oauth2/v2/auth'; - - /** - * The base endpoint for token retrieval. - */ - private static readonly GOOGLE_OAUTH2_TOKEN_URL_ = - 'https://oauth2.googleapis.com/token'; - - /** - * The base endpoint to revoke tokens. - */ - private static readonly GOOGLE_OAUTH2_REVOKE_URL_ = - 'https://oauth2.googleapis.com/revoke'; - /** - * Google Sign on certificates in PEM format. - */ - private static readonly GOOGLE_OAUTH2_FEDERATED_SIGNON_PEM_CERTS_URL_ = - 'https://www.googleapis.com/oauth2/v1/certs'; + this.endpoints = { + tokenInfoUrl: 'https://oauth2.googleapis.com/tokeninfo', + oauth2AuthBaseUrl: 'https://accounts.google.com/o/oauth2/v2/auth', + oauth2TokenUrl: 'https://oauth2.googleapis.com/token', + oauth2RevokeUrl: 'https://oauth2.googleapis.com/revoke', + oauth2FederatedSignonPemCertsUrl: + 'https://www.googleapis.com/oauth2/v1/certs', + oauth2FederatedSignonJwkCertsUrl: + 'https://www.googleapis.com/oauth2/v3/certs', + oauth2IapPublicKeyUrl: 'https://www.gstatic.com/iap/verify/public_key', + ...opts.endpoints, + }; - /** - * Google Sign on certificates in JWK format. - */ - private static readonly GOOGLE_OAUTH2_FEDERATED_SIGNON_JWK_CERTS_URL_ = - 'https://www.googleapis.com/oauth2/v3/certs'; + this.issuers = opts.issuers || [ + 'accounts.google.com', + 'https://accounts.google.com', + ]; + } /** - * Google Sign on certificates in JWK format. + * @deprecated */ - private static readonly GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_ = - 'https://www.gstatic.com/iap/verify/public_key'; + protected static readonly GOOGLE_TOKEN_INFO_URL = + 'https://oauth2.googleapis.com/tokeninfo'; /** * Clock skew - five minutes in seconds @@ -507,17 +561,9 @@ export class OAuth2Client extends AuthClient { private static readonly CLOCK_SKEW_SECS_ = 300; /** - * Max Token Lifetime is one day in seconds + * The default max Token Lifetime is one day in seconds */ - private static readonly MAX_TOKEN_LIFETIME_SECS_ = 86400; - - /** - * The allowed oauth token issuers. - */ - private static readonly ISSUERS_ = [ - 'accounts.google.com', - 'https://accounts.google.com', - ]; + private static readonly DEFAULT_MAX_TOKEN_LIFETIME_SECS_ = 86400; /** * Generates URL for consent page landing. @@ -537,7 +583,7 @@ export class OAuth2Client extends AuthClient { if (Array.isArray(opts.scope)) { opts.scope = opts.scope.join(' '); } - const rootUrl = OAuth2Client.GOOGLE_OAUTH2_AUTH_BASE_URL_; + const rootUrl = this.endpoints.oauth2AuthBaseUrl.toString(); return ( rootUrl + '?' + @@ -612,7 +658,7 @@ export class OAuth2Client extends AuthClient { private async getTokenAsync( options: GetTokenOptions ): Promise { - const url = OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_; + const url = this.endpoints.oauth2TokenUrl.toString(); const values = { code: options.code, client_id: options.client_id || this._clientId, @@ -673,7 +719,7 @@ export class OAuth2Client extends AuthClient { if (!refreshToken) { throw new Error('No refresh token is set.'); } - const url = OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_; + const url = this.endpoints.oauth2TokenUrl.toString(); const data = { refresh_token: refreshToken, client_id: this._clientId, @@ -874,10 +920,19 @@ export class OAuth2Client extends AuthClient { /** * Generates an URL to revoke the given token. * @param token The existing token to be revoked. + * + * @deprecated use instance method {@link OAuth2Client.getRevokeTokenURL} */ static getRevokeTokenUrl(token: string): string { - const parameters = querystring.stringify({token}); - return `${OAuth2Client.GOOGLE_OAUTH2_REVOKE_URL_}?${parameters}`; + return new OAuth2Client().getRevokeTokenURL(token).toString(); + } + + getRevokeTokenURL(token: string): URL { + const url = new URL(this.endpoints.oauth2RevokeUrl); + + url.searchParams.append('token', token); + + return url; } /** @@ -895,7 +950,7 @@ export class OAuth2Client extends AuthClient { callback?: BodyResponseCallback ): GaxiosPromise | void { const opts: GaxiosOptions = { - url: OAuth2Client.getRevokeTokenUrl(token), + url: this.getRevokeTokenURL(token).toString(), method: 'POST', }; if (callback) { @@ -1080,7 +1135,7 @@ export class OAuth2Client extends AuthClient { options.idToken, response.certs, options.audience, - OAuth2Client.ISSUERS_, + this.issuers, options.maxExpiry ); @@ -1101,7 +1156,7 @@ export class OAuth2Client extends AuthClient { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Bearer ${accessToken}`, }, - url: OAuth2Client.GOOGLE_TOKEN_INFO_URL, + url: this.endpoints.tokenInfoUrl.toString(), }); const info = Object.assign( { @@ -1152,10 +1207,10 @@ export class OAuth2Client extends AuthClient { let url: string; switch (format) { case CertificateFormat.PEM: - url = OAuth2Client.GOOGLE_OAUTH2_FEDERATED_SIGNON_PEM_CERTS_URL_; + url = this.endpoints.oauth2FederatedSignonPemCertsUrl.toString(); break; case CertificateFormat.JWK: - url = OAuth2Client.GOOGLE_OAUTH2_FEDERATED_SIGNON_JWK_CERTS_URL_; + url = this.endpoints.oauth2FederatedSignonJwkCertsUrl.toString(); break; default: throw new Error(`Unsupported certificate format ${format}`); @@ -1226,7 +1281,7 @@ export class OAuth2Client extends AuthClient { async getIapPublicKeysAsync(): Promise { let res: GaxiosResponse; - const url: string = OAuth2Client.GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_; + const url = this.endpoints.oauth2IapPublicKeyUrl.toString(); try { res = await this.transporter.request({url}); @@ -1269,7 +1324,7 @@ export class OAuth2Client extends AuthClient { const crypto = createCrypto(); if (!maxExpiry) { - maxExpiry = OAuth2Client.MAX_TOKEN_LIFETIME_SECS_; + maxExpiry = OAuth2Client.DEFAULT_MAX_TOKEN_LIFETIME_SECS_; } const segments = jwt.split('.'); diff --git a/test/test.baseexternalclient.ts b/test/test.baseexternalclient.ts index 96ea57ce..bfe5f680 100644 --- a/test/test.baseexternalclient.ts +++ b/test/test.baseexternalclient.ts @@ -171,43 +171,6 @@ describe('BaseExternalAccountClient', () => { }, expectedError); }); - const invalidWorkforceAudiences = [ - '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/provider', - '//iam.googleapis.com/locations/global/workforcepools/pool/providers/provider', - '//iam.googleapis.com/locations/global/workforcePools//providers/provider', - '//iam.googleapis.com/locations/global/workforcePools/providers/provider', - '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/provider', - '//iam.googleapis.com//locations/global/workforcePools/pool/providers/provider', - '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/provider', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers', - '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/provider', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', - '//iam.googleapis.com/locations//workforcePools/pool/providers/provider', - '//iam.googleapis.com/locations/workforcePools/pool/providers/provider', - '//iamAgoogleapisAcom/locations/global/workforcePools/workloadPools/providers/oidc', - ]; - const invalidExternalAccountOptionsWorkforceUserProject = Object.assign( - {}, - externalAccountOptionsWorkforceUserProject - ); - const expectedWorkforcePoolUserProjectError = new Error( - 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' - ); - - invalidWorkforceAudiences.forEach(invalidWorkforceAudience => { - it(`should throw given audience ${invalidWorkforceAudience} with user project defined in options`, () => { - invalidExternalAccountOptionsWorkforceUserProject.audience = - invalidWorkforceAudience; - - assert.throws(() => { - return new TestExternalAccountClient( - invalidExternalAccountOptionsWorkforceUserProject - ); - }, expectedWorkforcePoolUserProjectError); - }); - }); - it('should not throw on valid workforce audience configs', () => { const validWorkforceAudiences = [ '//iam.googleapis.com/locations/global/workforcePools/workforcePools/providers/provider', diff --git a/test/test.externalclient.ts b/test/test.externalclient.ts index 4a083dff..30cf9013 100644 --- a/test/test.externalclient.ts +++ b/test/test.externalclient.ts @@ -93,21 +93,6 @@ describe('ExternalAccountClient', () => { forceRefreshOnFailure: true, }; - const invalidWorkforceIdentityPoolClientAudiences = [ - '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcepools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools//providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/providers/oidc', - '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/oidc', - '//iam.googleapis.com//locations/global/workforcePools/pool/providers/oidc', - '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', - '//iam.googleapis.com/locations//workforcePools/pool/providers/oidc', - '//iam.googleapis.com/locations/workforcePools/pool/providers/oidc', - ]; - it('should return IdentityPoolClient on IdentityPoolClientOptions', () => { const expectedClient = new IdentityPoolClient(fileSourcedOptions); @@ -201,29 +186,6 @@ describe('ExternalAccountClient', () => { ); }); - invalidWorkforceIdentityPoolClientAudiences.forEach( - invalidWorkforceIdentityPoolClientAudience => { - const workforceIdentityPoolClientInvalidOptions = Object.assign( - {}, - fileSourcedOptions, - { - workforce_pool_user_project: 'workforce_pool_user_project', - subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', - } - ); - it(`should throw an error when an invalid workforce audience ${invalidWorkforceIdentityPoolClientAudience} is provided with a workforce user project`, () => { - workforceIdentityPoolClientInvalidOptions.audience = - invalidWorkforceIdentityPoolClientAudience; - - assert.throws(() => { - return ExternalAccountClient.fromJSON( - workforceIdentityPoolClientInvalidOptions - ); - }); - }); - } - ); - it('should return null when given non-ExternalAccountClientOptions', () => { assert( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/test/test.identitypoolclient.ts b/test/test.identitypoolclient.ts index 1faa5bdd..a2bdcacb 100644 --- a/test/test.identitypoolclient.ts +++ b/test/test.identitypoolclient.ts @@ -179,29 +179,6 @@ describe('IdentityPoolClient', () => { }); describe('Constructor', () => { - const invalidWorkforceIdentityPoolClientAudiences = [ - '//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcepools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools//providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/providers/oidc', - '//iam.googleapis.com/locations/global/workloadIdentityPools/workforcePools/pool/providers/oidc', - '//iam.googleapis.com//locations/global/workforcePools/pool/providers/oidc', - '//iam.googleapis.com/project/123/locations/global/workforcePools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/workloadIdentityPools/pool/providers/oidc', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers', - '//iam.googleapis.com/locations/global/workforcePools/pool/providers/', - '//iam.googleapis.com/locations//workforcePools/pool/providers/oidc', - '//iam.googleapis.com/locations/workforcePools/pool/providers/oidc', - ]; - const invalidWorkforceIdentityPoolFileSourceOptions = Object.assign( - {}, - fileSourcedOptionsWithWorkforceUserProject - ); - const expectedWorkforcePoolUserProjectError = new Error( - 'workforcePoolUserProject should not be set for non-workforce pool ' + - 'credentials.' - ); - it('should throw when neither file or url sources are provided', () => { const expectedError = new Error( 'No valid Identity Pool "credential_source" provided, must be either file or url.' @@ -287,21 +264,6 @@ describe('IdentityPoolClient', () => { }, expectedError); }); - invalidWorkforceIdentityPoolClientAudiences.forEach( - invalidWorkforceIdentityPoolClientAudience => { - it(`should throw given audience ${invalidWorkforceIdentityPoolClientAudience} with user project defined in IdentityPoolClientOptions`, () => { - invalidWorkforceIdentityPoolFileSourceOptions.audience = - invalidWorkforceIdentityPoolClientAudience; - - assert.throws(() => { - return new IdentityPoolClient( - invalidWorkforceIdentityPoolFileSourceOptions - ); - }, expectedWorkforcePoolUserProjectError); - }); - } - ); - it('should not throw when valid file-sourced options are provided', () => { assert.doesNotThrow(() => { return new IdentityPoolClient(fileSourcedOptions); From d9e8d8881740e4b1caa4e37e92611aa74ec6ba82 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 5 Jan 2024 18:08:39 -0800 Subject: [PATCH 3/7] docs: Update --- src/auth/jwtclient.ts | 4 +++- src/auth/oauth2client.ts | 9 ++++++-- test/test.jwt.ts | 47 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index d25f4146..85bfa886 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -24,6 +24,7 @@ import { OAuth2ClientOptions, RequestMetadataResponse, } from './oauth2client'; +import {DEFAULT_UNIVERSE} from './authclient'; export interface JWTOptions extends OAuth2ClientOptions { email?: string; @@ -119,7 +120,8 @@ export class JWT extends OAuth2Client implements IdTokenProvider { url = this.defaultServicePath ? `https://${this.defaultServicePath}/` : url; const useSelfSignedJWT = (!this.hasUserScopes() && url) || - (this.useJWTAccessWithScope && this.hasAnyScopes()); + (this.useJWTAccessWithScope && this.hasAnyScopes()) || + this.universeDomain !== DEFAULT_UNIVERSE; if (!this.apiKey && useSelfSignedJWT) { if ( this.additionalClaims && diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index fe233d09..3e1cd7f1 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -406,7 +406,7 @@ export interface VerifyIdTokenOptions { export interface OAuth2ClientEndpoints { /** - * The + * The endpoint for viewing access token information * * @example * 'https://oauth2.googleapis.com/tokeninfo' @@ -550,7 +550,7 @@ export class OAuth2Client extends AuthClient { } /** - * @deprecated + * @deprecated use instance's {@link OAuth2Client.endpoints} */ protected static readonly GOOGLE_TOKEN_INFO_URL = 'https://oauth2.googleapis.com/tokeninfo'; @@ -927,6 +927,11 @@ export class OAuth2Client extends AuthClient { return new OAuth2Client().getRevokeTokenURL(token).toString(); } + /** + * Generates a URL to revoke the given token. + * + * @param token The existing token to be revoked. + */ getRevokeTokenURL(token: string): URL { const url = new URL(this.endpoints.oauth2RevokeUrl); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index f20fcbe5..1d8ee8d9 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -1007,6 +1007,53 @@ describe('jwt', () => { ); }); + it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, universeDomain = not default universe', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + universeDomain: 'my-universe.com', + }); + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + undefined + ); + }); + + it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true, universeDomain = not default universe', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + universeDomain: 'my-universe.com', + }); + jwt.useJWTAccessWithScope = true; + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + ['scope1', 'scope2'] + ); + }); + it('does not use self signed JWT if target_audience provided', async () => { const JWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: sinon.stub().returns({}), From c30cdf96847a8b2fe399ae8ebaedb08e7ae69041 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 5 Jan 2024 18:09:09 -0800 Subject: [PATCH 4/7] docs: Update --- src/auth/oauth2client.ts | 9 ++++++-- test/test.jwt.ts | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index fe233d09..3e1cd7f1 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -406,7 +406,7 @@ export interface VerifyIdTokenOptions { export interface OAuth2ClientEndpoints { /** - * The + * The endpoint for viewing access token information * * @example * 'https://oauth2.googleapis.com/tokeninfo' @@ -550,7 +550,7 @@ export class OAuth2Client extends AuthClient { } /** - * @deprecated + * @deprecated use instance's {@link OAuth2Client.endpoints} */ protected static readonly GOOGLE_TOKEN_INFO_URL = 'https://oauth2.googleapis.com/tokeninfo'; @@ -927,6 +927,11 @@ export class OAuth2Client extends AuthClient { return new OAuth2Client().getRevokeTokenURL(token).toString(); } + /** + * Generates a URL to revoke the given token. + * + * @param token The existing token to be revoked. + */ getRevokeTokenURL(token: string): URL { const url = new URL(this.endpoints.oauth2RevokeUrl); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index f20fcbe5..1d8ee8d9 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -1007,6 +1007,53 @@ describe('jwt', () => { ); }); + it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, universeDomain = not default universe', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + universeDomain: 'my-universe.com', + }); + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + undefined + ); + }); + + it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true, universeDomain = not default universe', async () => { + const stubGetRequestHeaders = sandbox.stub().returns({}); + const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ + getRequestHeaders: stubGetRequestHeaders, + }); + const jwt = new JWT({ + email: 'foo@serviceaccount.com', + key: fs.readFileSync(PEM_PATH, 'utf8'), + scopes: ['scope1', 'scope2'], + subject: 'bar@subjectaccount.com', + universeDomain: 'my-universe.com', + }); + jwt.useJWTAccessWithScope = true; + jwt.defaultScopes = ['scope1', 'scope2']; + await jwt.getRequestHeaders('https//beepboop.googleapis.com'); + sandbox.assert.calledOnce(stubJWTAccess); + sandbox.assert.calledWith( + stubGetRequestHeaders, + 'https//beepboop.googleapis.com', + undefined, + ['scope1', 'scope2'] + ); + }); + it('does not use self signed JWT if target_audience provided', async () => { const JWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: sinon.stub().returns({}), From bd9d30ef5ba27bb69c30eb6e69d1507b690f2c71 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 5 Jan 2024 18:11:12 -0800 Subject: [PATCH 5/7] docs: Update --- src/auth/oauth2client.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index fe233d09..3e1cd7f1 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -406,7 +406,7 @@ export interface VerifyIdTokenOptions { export interface OAuth2ClientEndpoints { /** - * The + * The endpoint for viewing access token information * * @example * 'https://oauth2.googleapis.com/tokeninfo' @@ -550,7 +550,7 @@ export class OAuth2Client extends AuthClient { } /** - * @deprecated + * @deprecated use instance's {@link OAuth2Client.endpoints} */ protected static readonly GOOGLE_TOKEN_INFO_URL = 'https://oauth2.googleapis.com/tokeninfo'; @@ -927,6 +927,11 @@ export class OAuth2Client extends AuthClient { return new OAuth2Client().getRevokeTokenURL(token).toString(); } + /** + * Generates a URL to revoke the given token. + * + * @param token The existing token to be revoked. + */ getRevokeTokenURL(token: string): URL { const url = new URL(this.endpoints.oauth2RevokeUrl); From 6778647135c660fc9b2f86bb62f0b6395d128084 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Fri, 5 Jan 2024 18:15:42 -0800 Subject: [PATCH 6/7] chore: remove unrelated --- src/auth/jwtclient.ts | 4 +--- test/test.jwt.ts | 47 ------------------------------------------- 2 files changed, 1 insertion(+), 50 deletions(-) diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 85bfa886..d25f4146 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -24,7 +24,6 @@ import { OAuth2ClientOptions, RequestMetadataResponse, } from './oauth2client'; -import {DEFAULT_UNIVERSE} from './authclient'; export interface JWTOptions extends OAuth2ClientOptions { email?: string; @@ -120,8 +119,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider { url = this.defaultServicePath ? `https://${this.defaultServicePath}/` : url; const useSelfSignedJWT = (!this.hasUserScopes() && url) || - (this.useJWTAccessWithScope && this.hasAnyScopes()) || - this.universeDomain !== DEFAULT_UNIVERSE; + (this.useJWTAccessWithScope && this.hasAnyScopes()); if (!this.apiKey && useSelfSignedJWT) { if ( this.additionalClaims && diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 1d8ee8d9..f20fcbe5 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -1007,53 +1007,6 @@ describe('jwt', () => { ); }); - it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, universeDomain = not default universe', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); - const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ - getRequestHeaders: stubGetRequestHeaders, - }); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: fs.readFileSync(PEM_PATH, 'utf8'), - scopes: ['scope1', 'scope2'], - subject: 'bar@subjectaccount.com', - universeDomain: 'my-universe.com', - }); - jwt.defaultScopes = ['scope1', 'scope2']; - await jwt.getRequestHeaders('https//beepboop.googleapis.com'); - sandbox.assert.calledOnce(stubJWTAccess); - sandbox.assert.calledWith( - stubGetRequestHeaders, - 'https//beepboop.googleapis.com', - undefined, - undefined - ); - }); - - it('signs JWT with audience if: user scope = true, default scope = true, audience = truthy, useJWTAccessWithScope = true, universeDomain = not default universe', async () => { - const stubGetRequestHeaders = sandbox.stub().returns({}); - const stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ - getRequestHeaders: stubGetRequestHeaders, - }); - const jwt = new JWT({ - email: 'foo@serviceaccount.com', - key: fs.readFileSync(PEM_PATH, 'utf8'), - scopes: ['scope1', 'scope2'], - subject: 'bar@subjectaccount.com', - universeDomain: 'my-universe.com', - }); - jwt.useJWTAccessWithScope = true; - jwt.defaultScopes = ['scope1', 'scope2']; - await jwt.getRequestHeaders('https//beepboop.googleapis.com'); - sandbox.assert.calledOnce(stubJWTAccess); - sandbox.assert.calledWith( - stubGetRequestHeaders, - 'https//beepboop.googleapis.com', - undefined, - ['scope1', 'scope2'] - ); - }); - it('does not use self signed JWT if target_audience provided', async () => { const JWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({ getRequestHeaders: sinon.stub().returns({}), From 322a4e622fdafcc7d21580b3d2d4ef8780a5a24d Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 23 Jan 2024 14:47:18 -0800 Subject: [PATCH 7/7] refactor: base endpoints on universe domain --- src/auth/baseexternalclient.ts | 13 +++++++++---- src/auth/downscopedclient.ts | 7 +------ src/auth/googleauth.ts | 7 ++++++- src/auth/oauth2client.ts | 13 ++++++------- test/test.googleauth.ts | 4 +++- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 6d69ea79..89bb610a 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -53,8 +53,12 @@ export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000; * 3. external_Account => non-GCP service (eg. AWS, Azure, K8s) */ export const EXTERNAL_ACCOUNT_TYPE = 'external_account'; -/** Cloud resource manager URL used to retrieve project information. */ -export const DEFAULT_CLOUD_RESOURCE_MANAGER = +/** + * Cloud resource manager URL used to retrieve project information. + * + * @deprecated use {@link BaseExternalAccountClient.cloudResourceManagerURL} instead + **/ +export const CLOUD_RESOURCE_MANAGER = 'https://cloudresourcemanager.googleapis.com/v1/projects/'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -159,7 +163,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { * new URL('https://cloudresourcemanager.googleapis.com/v1/projects/'); * ``` */ - protected cloudResourceManagerURL = new URL(DEFAULT_CLOUD_RESOURCE_MANAGER); + protected cloudResourceManagerURL: URL | string; /** * Instantiate a BaseExternalAccountClient instance using the provided JSON * object loaded from an external account credentials file. @@ -206,7 +210,8 @@ export abstract class BaseExternalAccountClient extends AuthClient { ).get('token_lifetime_seconds'); this.cloudResourceManagerURL = new URL( - opts.get('cloud_resource_manager_url') || DEFAULT_CLOUD_RESOURCE_MANAGER + opts.get('cloud_resource_manager_url') || + `https://cloudresourcemanager.${this.universeDomain}/v1/projects/` ); if (clientId) { diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index da06d87f..b4a7a019 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -40,11 +40,6 @@ const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; */ const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; -/** - * The default STS access token exchange endpoint. - **/ -const DEFAULT_STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1/token'; - /** * The maximum number of access boundary rules a Credential Access Boundary * can contain. @@ -179,7 +174,7 @@ export class DownscopedClient extends AuthClient { } this.stsCredential = new sts.StsCredentials( - tokenURL || DEFAULT_STS_ACCESS_TOKEN_URL + tokenURL || `https://sts.${this.universeDomain}/v1/token` ); this.cachedDownscopedAccessToken = null; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index a5f78f74..121c5e32 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -1069,6 +1069,11 @@ export class GoogleAuth { */ async sign(data: string, endpoint?: string): Promise { const client = await this.getClient(); + const universe = await this.getUniverseDomain(); + + endpoint = + endpoint || + `https://iamcredentials.${universe}/v1/projects/-/serviceAccounts/`; if (client instanceof Impersonated) { const signed = await client.sign(data); @@ -1093,7 +1098,7 @@ export class GoogleAuth { crypto: Crypto, emailOrUniqueId: string, data: string, - endpoint = 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' + endpoint: string ): Promise { const url = new URL(endpoint + `${emailOrUniqueId}:signBlob`); const res = await this.request({ diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 3e1cd7f1..9a12f509 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -531,14 +531,12 @@ export class OAuth2Client extends AuthClient { this.redirectUri = opts.redirectUri; this.endpoints = { - tokenInfoUrl: 'https://oauth2.googleapis.com/tokeninfo', + tokenInfoUrl: `https://oauth2.${this.universeDomain}/tokeninfo`, oauth2AuthBaseUrl: 'https://accounts.google.com/o/oauth2/v2/auth', - oauth2TokenUrl: 'https://oauth2.googleapis.com/token', - oauth2RevokeUrl: 'https://oauth2.googleapis.com/revoke', - oauth2FederatedSignonPemCertsUrl: - 'https://www.googleapis.com/oauth2/v1/certs', - oauth2FederatedSignonJwkCertsUrl: - 'https://www.googleapis.com/oauth2/v3/certs', + oauth2TokenUrl: `https://oauth2.${this.universeDomain}/token`, + oauth2RevokeUrl: `https://oauth2.${this.universeDomain}/revoke`, + oauth2FederatedSignonPemCertsUrl: `https://www.${this.universeDomain}/oauth2/v1/certs`, + oauth2FederatedSignonJwkCertsUrl: `https://www.${this.universeDomain}/oauth2/v3/certs`, oauth2IapPublicKeyUrl: 'https://www.gstatic.com/iap/verify/public_key', ...opts.endpoints, }; @@ -546,6 +544,7 @@ export class OAuth2Client extends AuthClient { this.issuers = opts.issuers || [ 'accounts.google.com', 'https://accounts.google.com', + this.universeDomain, ]; } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index c22057ef..5b1cb09c 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -1404,8 +1404,10 @@ describe('googleauth', () => { ) .resolves(); + const universe = await auth.getUniverseDomain(); + const email = 'google@auth.library'; - const iamUri = 'https://iamcredentials.googleapis.com'; + const iamUri = `https://iamcredentials.${universe}`; const iamPath = `/v1/projects/-/serviceAccounts/${email}:signBlob`; const signedBlob = 'erutangis'; const data = 'abc123';