import { GaxiosError , GaxiosOptions , GaxiosPromise , GaxiosResponse , } from 'gaxios' ; import * as stream from 'stream' ; import { Credentials } from './credentials' ; import { AuthClient } from './authclient' ; import { BodyResponseCallback } from '../transporters' ; import { GetAccessTokenResponse , Headers , RefreshOptions } from './oauth2client' ; import * as sts from './stscredentials' ; import { ClientAuthentication } from './oauth2common' ; /**
 * The required token exchange grant_type: rfc8693#section-2.1
 */ const STS_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange' ; /**
 * The requested token exchange requested_token_type: rfc8693#section-2.1
 */ const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token' ; /** The default OAuth scope to request when none is provided. */ const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform' ; /** The google apis domain pattern. */ const GOOGLE_APIS_DOMAIN_PATTERN = '\\.googleapis\\.com$' ; /** The variable portion pattern in a Google APIs domain. */ const VARIABLE_PORTION_PATTERN = '[^\\.\\s\\/\\\\]+' ; /**
 * Offset to take into account network delays and server clock skews.
 */ export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000 ; /**
 * The credentials JSON file type for external account clients.
 * There are 3 types of JSON configs:
 * 1. authorized_user => Google end user credential
 * 2. service_account => Google service account credential
 * 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 CLOUD_RESOURCE_MANAGER = 'https://cloudresourcemanager.googleapis.com/v1/projects/' ; /** The workforce audience pattern. */ const WORKFORCE_AUDIENCE_PATTERN = '//iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+' ; /**
 * Base external account credentials json interface.
 */ export interface BaseExternalAccountClientOptions { type : string ; audience : string ; subject_token_type : string ; service_account_impersonation_url ? : string ; token_url : string ; token_info_url ? : string ; client_id ? : string ; client_secret ? : string ; quota_project_id ? : string ; workforce_pool_user_project ? : string ; } /**
 * Interface defining the successful response for iamcredentials
 * generateAccessToken API.
 * https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
 */ export interface IamGenerateAccessTokenResponse { accessToken : string ; expireTime : string ; } /**
 * Interface defining the project information response returned by the cloud
 * resource manager.
 * https://cloud.google.com/resource-manager/reference/rest/v1/projects#Project
 */ export interface ProjectInfo { projectNumber : string ; projectId : string ; lifecycleState : string ; name : string ; createTime ? : string ; parent : { [ key ] : any ; } ; } /**
 * Internal interface for tracking the access token expiration time.
 */ interface CredentialsWithResponse extends Credentials { res ? : GaxiosResponse | null ; } /**
 * Base external account client. This is used to instantiate AuthClients for
 * exchanging external account credentials for GCP access token and authorizing
 * requests to GCP APIs.
 * The base class implements common logic for exchanging various type of
 * external credentials for GCP access token. The logic of determining and
 * retrieving the external credential based on the environment and
 * credential_source will be left for the subclasses.
 */ export abstract class BaseExternalAccountClient extends AuthClient { /**
     * OAuth scopes for the GCP access token to use. When not provided,
     * the default https://www.googleapis.com/auth/cloud-platform is
     * used.
     */ public scopes ? : string | string [ ] ; private cachedAccessToken : CredentialsWithResponse | null ; protected readonly audience : string ; private readonly subjectTokenType : string ; private readonly serviceAccountImpersonationUrl ? : string ; private readonly stsCredential : sts . StsCredentials ; private readonly clientAuth ? : ClientAuthentication ; private readonly workforcePoolUserProject ? : string ; public projectId : string | null ; public projectNumber : string | null ; public readonly eagerRefreshThresholdMillis : number ; public readonly forceRefreshOnFailure : boolean ; param options param additionalOptions constructor ( options , additionalOptions ? ) { super ( ) ; if ( options . type !== EXTERNAL_ACCOUNT_TYPE ) { throw new Error ( `Expected "${ EXTERNAL_ACCOUNT_TYPE }" type but ` + `received "${ options . type }"` ) ; } this . clientAuth = options . client_id ? ( { confidentialClientType : 'basic' , clientId : options . client_id , clientSecret : options . client_secret , } as ClientAuthentication ) : undefined ; if ( ! this . validateGoogleAPIsUrl ( 'sts' , options . token_url ) ) { throw new Error ( `"${ options . token_url }" is not a valid token url.` ) ; } this . stsCredential = new sts . StsCredentials ( options . token_url , this . clientAuth ) ; this . scopes = [ DEFAULT_OAUTH_SCOPE ] ; this . cachedAccessToken = null ; this . audience = options . audience ; this . subjectTokenType = options . subject_token_type ; this . quotaProjectId = options . quota_project_id ; this . workforcePoolUserProject = options . workforce_pool_user_project ; const workforceAudiencePattern = new RegExp ( WORKFORCE_AUDIENCE_PATTERN ) ; if ( this . workforcePoolUserProject && ! this . audience . match ( workforceAudiencePattern ) ) { throw new Error ( 'workforcePoolUserProject should not be set for non-workforce pool ' + 'credentials.' ) ; } if ( typeof options . service_account_impersonation_url !== 'undefined' && ! this . validateGoogleAPIsUrl ( 'iamcredentials' , options . service_account_impersonation_url ) ) { throw new Error ( `"${ options . service_account_impersonation_url }" is ` + 'not a valid service account impersonation url.' ) ; } this . serviceAccountImpersonationUrl = options . service_account_impersonation_url ; if ( typeof additionalOptions ?. eagerRefreshThresholdMillis !== 'number' ) { this . eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET ; } else { this . eagerRefreshThresholdMillis = additionalOptions ! . eagerRefreshThresholdMillis as number ; } this . forceRefreshOnFailure = ! ! additionalOptions ?. forceRefreshOnFailure ; this . projectId = null ; this . projectNumber = this . getProjectNumber ( this . audience ) ; } /** The service account email to be impersonated, if available. */ getServiceAccountEmail ( ) { if ( this . serviceAccountImpersonationUrl ) { const re = /serviceAccounts\/(?<email>[^:]+):generateAccessToken$/ ; const result = re . exec ( this . serviceAccountImpersonationUrl ) ; return result ?. groups ?. email || null ; } return null ; } param credentials setCredentials ( credentials ) { super . setCredentials ( credentials ) ; this . cachedAccessToken = credentials ; } return abstract async retrieveSubjectToken ( ) ; return async getAccessToken ( ) { if ( ! this . cachedAccessToken || this . isExpired ( this . cachedAccessToken ) ) { await this . refreshAccessTokenAsync ( ) ; } return { token : this . cachedAccessToken ! . access_token , res : this . cachedAccessToken ! . res , } ; } /**
     * 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.
     *
     * The result has the form:
     * { Authorization: 'Bearer <access_token_value>' }
     */ async getRequestHeaders ( ) { const accessTokenResponse = await this . getAccessToken ( ) ; const headers = { Authorization : `Bearer ${ accessTokenResponse . token }` , } ; return this . addSharedMetadataHeaders ( headers ) ; } param opts param callback return request < T > ( opts ) ; request < T > ( opts , callback ) ; request < T > ( opts , callback ? ) { if ( callback ) { this . requestAsync < T > ( opts ) . then ( r => callback ( null , r ) , e => { return callback ( e , e . response ) ; } ) ; } else { return this . requestAsync < T > ( opts ) ; } } return async getProjectId ( ) { const projectNumber = this . projectNumber || this . workforcePoolUserProject ; if ( this . projectId ) { return this . projectId ; } else if ( projectNumber ) { const headers = await this . getRequestHeaders ( ) ; const response = await this . transporter . request < ProjectInfo > ( { headers , url : `${ CLOUD_RESOURCE_MANAGER }${ projectNumber }` , responseType : 'json' , } ) ; this . projectId = response . data . projectId ; return this . projectId ; } return null ; } param opts param retry return protected async requestAsync < T > ( opts , retry = false ) { let response ; try { const requestHeaders = await this . getRequestHeaders ( ) ; opts . headers = opts . headers || { } ; if ( requestHeaders && requestHeaders [ 'x-goog-user-project' ] ) { opts . headers [ 'x-goog-user-project' ] = requestHeaders [ 'x-goog-user-project' ] ; } if ( requestHeaders && requestHeaders . Authorization ) { opts . headers . Authorization = requestHeaders . Authorization ; } response = await this . transporter . request < T > ( opts ) ; } catch ( e ) { const res = ( e as GaxiosError ) . response ; if ( res ) { const statusCode = res . status ; const isReadableStream = res . config . data instanceof stream . Readable ; const isAuthErr = statusCode === 401 || statusCode === 403 ; if ( ! retry && isAuthErr && ! isReadableStream && this . forceRefreshOnFailure ) { await this . refreshAccessTokenAsync ( ) ; return await this . requestAsync < T > ( opts , true ) ; } } throw e ; } return response ; } return protected async refreshAccessTokenAsync ( ) { const subjectToken = await this . retrieveSubjectToken ( ) ; const stsCredentialsOptions = { grantType : STS_GRANT_TYPE , audience : this . audience , requestedTokenType : STS_REQUEST_TOKEN_TYPE , subjectToken , subjectTokenType : this . subjectTokenType , scope : this . serviceAccountImpersonationUrl ? [ DEFAULT_OAUTH_SCOPE ] : this . getScopesArray ( ) , } ; const additionalOptions = ! this . clientAuth && this . workforcePoolUserProject ? { userProject : this . workforcePoolUserProject } : undefined ; const stsResponse = await this . stsCredential . exchangeToken ( stsCredentialsOptions , undefined , additionalOptions ) ; if ( this . serviceAccountImpersonationUrl ) { this . cachedAccessToken = await this . getImpersonatedAccessToken ( stsResponse . access_token ) ; } else if ( stsResponse . expires_in ) { this . cachedAccessToken = { access_token : stsResponse . access_token , expiry_date : new Date ( ) . getTime ( ) + stsResponse . expires_in * 1000 , res : stsResponse . res , } ; } else { this . cachedAccessToken = { access_token : stsResponse . access_token , res : stsResponse . res , } ; } this . credentials = { } ; Object . assign ( this . credentials , this . cachedAccessToken ) ; delete ( this . credentials as CredentialsWithResponse ) . res ; this . emit ( 'tokens' , { refresh_token : null , expiry_date : this . cachedAccessToken ! . expiry_date , access_token : this . cachedAccessToken ! . access_token , token_type : 'Bearer' , id_token : null , } ) ; return this . cachedAccessToken ; } param audience return private getProjectNumber ( audience ) { const match = audience . match ( /\/projects\/([^/]+)/ ) ; if ( ! match ) { return null ; } return match [ 1 ] ; } param token return private async getImpersonatedAccessToken ( token ) { const opts = { url : this . serviceAccountImpersonationUrl ! , method : 'POST' , headers : { 'Content-Type' : 'application/json' , Authorization : `Bearer ${ token }` , } , data : { scope : this . getScopesArray ( ) , } , responseType : 'json' , } ; const response = await this . transporter . request < IamGenerateAccessTokenResponse > ( opts ) ; const successResponse = response . data ; return { access_token : successResponse . accessToken , expiry_date : new Date ( successResponse . expireTime ) . getTime ( ) , res : response , } ; } param accessToken return private isExpired ( accessToken ) { const now = new Date ( ) . getTime ( ) ; return accessToken . expiry_date ? now >= accessToken . expiry_date - this . eagerRefreshThresholdMillis : false ; } return private getScopesArray ( ) { if ( typeof this . scopes === 'string' ) { return [ this . scopes ] ; } else if ( typeof this . scopes === 'undefined' ) { return [ DEFAULT_OAUTH_SCOPE ] ; } else { return this . scopes ; } } param apiName param url return private validateGoogleAPIsUrl ( apiName , url ) { let parsedUrl ; try { parsedUrl = new URL ( url ) ; } catch ( e ) { return false ; } const urlDomain = parsedUrl . hostname ; if ( parsedUrl . protocol !== 'https:' ) { return false ; } const googleAPIsDomainPatterns = [ new RegExp ( '^' + VARIABLE_PORTION_PATTERN + '\\.' + apiName + GOOGLE_APIS_DOMAIN_PATTERN ) , new RegExp ( '^' + apiName + GOOGLE_APIS_DOMAIN_PATTERN ) , new RegExp ( '^' + apiName + '\\.' + VARIABLE_PORTION_PATTERN + GOOGLE_APIS_DOMAIN_PATTERN ) , new RegExp ( '^' + VARIABLE_PORTION_PATTERN + '\\-' + apiName + GOOGLE_APIS_DOMAIN_PATTERN ) , ] ; for ( const googleAPIsDomainPattern of googleAPIsDomainPatterns ) { if ( urlDomain . match ( googleAPIsDomainPattern ) ) { return true ; } } return false ; } }