From 3651b291a30ebed5a4ddc553b7e61d59318bec77 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 9 Sep 2024 14:30:40 -0700 Subject: [PATCH] chore: WIP --- src/auth/authclient.ts | 51 ++++++++++++++++++- src/auth/baseexternalclient.ts | 7 ++- src/auth/computeclient.ts | 2 + src/auth/downscopedclient.ts | 2 +- .../externalAccountAuthorizedUserClient.ts | 2 +- src/auth/impersonated.ts | 1 + src/auth/jwtclient.ts | 31 ++++++++--- src/auth/oauth2client.ts | 21 ++++---- src/auth/passthrough.ts | 2 +- src/auth/refreshclient.ts | 4 +- src/transporters.ts | 2 +- 11 files changed, 97 insertions(+), 28 deletions(-) diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 2f2b384c..4ebe5816 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -187,6 +187,13 @@ export abstract class AuthClient forceRefreshOnFailure = false; universeDomain = DEFAULT_UNIVERSE; + /** + * The type of credential. + * + * @see {@link AuthClient.CREDENTIAL_TYPES} + */ + readonly credentialType?: keyof typeof AuthClient.CREDENTIAL_TYPES; + constructor(opts: AuthClientOptions = {}) { super(); @@ -234,9 +241,33 @@ export abstract class AuthClient } /** - * Provides an alternative Gaxios request implementation with auth credentials + * The public request API in which credentials may be added to the request. + * + * @param options options for `gaxios` + */ + abstract request(options: GaxiosOptions): GaxiosPromise; + + /** + * The internal request handler. At this stage the credentials have been created, but + * the standard headers have not been added until this call. + * + * @param options options for `gaxios` */ - abstract request(opts: GaxiosOptions): GaxiosPromise; + protected _request(options: GaxiosOptions): GaxiosPromise { + if (!options.headers?.['x-goog-api-client']) { + options.headers = options.headers || {}; + const nodeVersion = process.version.replace(/^v/, ''); + options.headers['x-goog-api-client'] = `gl-node/${nodeVersion}`; + } + + if (this.credentialType) { + options.headers['x-goog-api-client'] = + options.headers['x-goog-api-client'] + + ` cred-type/${this.credentialType}`; + } + + return this.transporter.request(options); + } /** * The main authentication interface. It takes an optional url which when @@ -286,6 +317,22 @@ export abstract class AuthClient return headers; } + /** + * An enum of the known credential types + */ + protected static CREDENTIAL_TYPES = { + /** for gcloud user credential (auth code flow and refresh flow) */ + u: 'u', + /** service account credential with assertion token flow */ + sa: 'sa', + /** service account credential with self signed jwt token flow */ + jwt: 'jwt', + /** service account credential attached to metadata server, i.e. VM credential */ + mds: 'mds', + /** impersonated credential */ + imp: 'imp', + } as const; + /** * Retry config for Auth-related requests. * diff --git a/src/auth/baseexternalclient.ts b/src/auth/baseexternalclient.ts index 26436979..46b84ccc 100644 --- a/src/auth/baseexternalclient.ts +++ b/src/auth/baseexternalclient.ts @@ -478,7 +478,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { } else if (projectNumber) { // Preferable not to use request() to avoid retrial policies. const headers = await this.getRequestHeaders(); - const response = await this.transporter.request({ + const response = await this._request({ ...BaseExternalAccountClient.RETRY_CONFIG, headers, url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`, @@ -512,7 +512,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { if (requestHeaders && requestHeaders.Authorization) { opts.headers.Authorization = requestHeaders.Authorization; } - response = await this.transporter.request(opts); + response = await this._request(opts); } catch (e) { const res = (e as GaxiosError).response; if (res) { @@ -679,8 +679,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { }, responseType: 'json', }; - const response = - await this.transporter.request(opts); + const response = await this._request(opts); const successResponse = response.data; return { access_token: successResponse.accessToken, diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index fbcec074..6a8b16dd 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -37,6 +37,8 @@ export interface ComputeOptions extends OAuth2ClientOptions { } export class Compute extends OAuth2Client { + credentialType = Compute.CREDENTIAL_TYPES.mds; + readonly serviceAccountEmail: string; scopes: string[]; diff --git a/src/auth/downscopedclient.ts b/src/auth/downscopedclient.ts index 3005e7cc..8a179662 100644 --- a/src/auth/downscopedclient.ts +++ b/src/auth/downscopedclient.ts @@ -268,7 +268,7 @@ export class DownscopedClient extends AuthClient { if (requestHeaders && requestHeaders.Authorization) { opts.headers.Authorization = requestHeaders.Authorization; } - response = await this.transporter.request(opts); + response = await this._request(opts); } catch (e) { const res = (e as GaxiosError).response; if (res) { diff --git a/src/auth/externalAccountAuthorizedUserClient.ts b/src/auth/externalAccountAuthorizedUserClient.ts index 24a480c0..903fc992 100644 --- a/src/auth/externalAccountAuthorizedUserClient.ts +++ b/src/auth/externalAccountAuthorizedUserClient.ts @@ -268,7 +268,7 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient { if (requestHeaders && requestHeaders.Authorization) { opts.headers.Authorization = requestHeaders.Authorization; } - response = await this.transporter.request(opts); + response = await this._request(opts); } catch (e) { const res = (e as GaxiosError).response; if (res) { diff --git a/src/auth/impersonated.ts b/src/auth/impersonated.ts index 2b4a2555..fe8c9a6e 100644 --- a/src/auth/impersonated.ts +++ b/src/auth/impersonated.ts @@ -72,6 +72,7 @@ export interface FetchIdTokenResponse { } export class Impersonated extends OAuth2Client implements IdTokenProvider { + credentialType = Impersonated.CREDENTIAL_TYPES.imp; private sourceClient: AuthClient; private targetPrincipal: string; private targetScopes: string[]; diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index a2d753d2..454de752 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -37,6 +37,8 @@ export interface JWTOptions extends OAuth2ClientOptions { } export class JWT extends OAuth2Client implements IdTokenProvider { + credentialType!: (typeof JWT.CREDENTIAL_TYPES)['jwt' | 'sa']; + email?: string; keyFile?: string; key?: string; @@ -95,6 +97,9 @@ export class JWT extends OAuth2Client implements IdTokenProvider { // Start with an expired refresh token, which will automatically be // refreshed before the first API call is made. this.credentials = {refresh_token: 'jwt-placeholder', expiry_date: 1}; + + // Using to set the `credentialType` + this.#useSelfSigned(); } /** @@ -109,6 +114,24 @@ export class JWT extends OAuth2Client implements IdTokenProvider { return jwt; } + #useSelfSigned(url?: string | null): boolean { + url = this.defaultServicePath ? `https://${this.defaultServicePath}/` : url; + + const useSelfSignedFlow = + !this.apiKey && + !!( + (!this.hasUserScopes() && url) || + (this.useJWTAccessWithScope && this.hasAnyScopes()) || + this.universeDomain !== DEFAULT_UNIVERSE + ); + + this.credentialType = useSelfSignedFlow + ? JWT.CREDENTIAL_TYPES.jwt + : JWT.CREDENTIAL_TYPES.sa; + + return useSelfSignedFlow; + } + /** * Obtains the metadata to be sent with the request. * @@ -117,19 +140,13 @@ export class JWT extends OAuth2Client implements IdTokenProvider { protected async getRequestMetadataAsync( url?: string | null ): Promise { - url = this.defaultServicePath ? `https://${this.defaultServicePath}/` : url; - const useSelfSignedJWT = - (!this.hasUserScopes() && url) || - (this.useJWTAccessWithScope && this.hasAnyScopes()) || - this.universeDomain !== DEFAULT_UNIVERSE; - if (this.subject && this.universeDomain !== DEFAULT_UNIVERSE) { throw new RangeError( `Service Account user is configured for the credential. Domain-wide delegation is not supported in universes other than ${DEFAULT_UNIVERSE}` ); } - if (!this.apiKey && useSelfSignedJWT) { + if (!this.#useSelfSigned(url)) { if ( this.additionalClaims && ( diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index fb813a7f..f833915a 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -708,7 +708,7 @@ export class OAuth2Client extends AuthClient { if (this.clientAuthentication === ClientAuthentication.ClientSecretPost) { values.client_secret = this._clientSecret; } - const res = await this.transporter.request({ + const res = await this._request({ ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, @@ -773,7 +773,7 @@ export class OAuth2Client extends AuthClient { try { // request for new token - res = await this.transporter.request({ + res = await this._request({ ...OAuth2Client.RETRY_CONFIG, method: 'POST', url, @@ -1003,11 +1003,12 @@ export class OAuth2Client extends AuthClient { method: 'POST', }; if (callback) { - this.transporter - .request(opts) - .then(r => callback(null, r), callback); + this._request(opts).then( + r => callback(null, r), + callback + ); } else { - return this.transporter.request(opts); + return this._request(opts); } } @@ -1082,7 +1083,7 @@ export class OAuth2Client extends AuthClient { if (this.apiKey) { opts.headers['X-Goog-Api-Key'] = this.apiKey; } - r2 = await this.transporter.request(opts); + r2 = await this._request(opts); } catch (e) { const res = (e as GaxiosError).response; if (res) { @@ -1204,7 +1205,7 @@ export class OAuth2Client extends AuthClient { * user info. */ async getTokenInfo(accessToken: string): Promise { - const {data} = await this.transporter.request({ + const {data} = await this._request({ ...OAuth2Client.RETRY_CONFIG, method: 'POST', headers: { @@ -1271,7 +1272,7 @@ export class OAuth2Client extends AuthClient { throw new Error(`Unsupported certificate format ${format}`); } try { - res = await this.transporter.request({ + res = await this._request({ ...OAuth2Client.RETRY_CONFIG, url, }); @@ -1342,7 +1343,7 @@ export class OAuth2Client extends AuthClient { const url = this.endpoints.oauth2IapPublicKeyUrl.toString(); try { - res = await this.transporter.request({ + res = await this._request({ ...OAuth2Client.RETRY_CONFIG, url, }); diff --git a/src/auth/passthrough.ts b/src/auth/passthrough.ts index bde50cba..4fc0d007 100644 --- a/src/auth/passthrough.ts +++ b/src/auth/passthrough.ts @@ -36,7 +36,7 @@ export class PassThroughClient extends AuthClient { * @returns The response of the request. */ async request(opts: GaxiosOptions) { - return this.transporter.request(opts); + return this._request(opts); } /** diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index eca95d1b..27c22654 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -35,6 +35,8 @@ export class UserRefreshClient extends OAuth2Client { // This is also a hard one because `this.refreshToken` is a function. _refreshToken?: string | null; + credentialType = UserRefreshClient.CREDENTIAL_TYPES.u; + /** * User Refresh Token credentials. * @@ -80,7 +82,7 @@ export class UserRefreshClient extends OAuth2Client { } async fetchIdToken(targetAudience: string): Promise { - const res = await this.transporter.request({ + const res = await this._request({ ...UserRefreshClient.RETRY_CONFIG, url: this.endpoints.oauth2TokenUrl, headers: { diff --git a/src/transporters.ts b/src/transporters.ts index 41660308..18cc94f3 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -67,7 +67,7 @@ export class DefaultTransporter implements Transporter { opts.headers['User-Agent'] = `${uaValue} ${DefaultTransporter.USER_AGENT}`; } - // track google-auth-library-nodejs version: + if (!opts.headers['x-goog-api-client']) { const nodeVersion = process.version.replace(/^v/, ''); opts.headers['x-goog-api-client'] = `gl-node/${nodeVersion}`;