import { GaxiosError , GaxiosOptions , GaxiosPromise , GaxiosResponse , } from 'gaxios' ; import * as stream from 'stream' ; import { BodyResponseCallback } from '../transporters' ; import { Credentials } from './credentials' ; import { AuthClient } from './authclient' ; import { GetAccessTokenResponse , Headers , RefreshOptions } from './oauth2client' ; import * as sts from './stscredentials' ; /**
 * 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 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 maximum number of access boundary rules a Credential Access Boundary
 * can contain.
 */ export const MAX_ACCESS_BOUNDARY_RULES_COUNT = 10 ; /**
 * Offset to take into account network delays and server clock skews.
 */ export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000 ; /**
 * Internal interface for tracking the access token expiration time.
 */ interface CredentialsWithResponse extends Credentials { res ? : GaxiosResponse | null ; } /**
 * Internal interface for tracking and returning the Downscoped access token
 * expiration time in epoch time (seconds).
 */ interface DownscopedAccessTokenResponse extends GetAccessTokenResponse { expirationTime ? : number | null ; } /**
 * Defines an upper bound of permissions available for a GCP credential.
 */ export interface CredentialAccessBoundary { accessBoundary : { accessBoundaryRules : AccessBoundaryRule [ ] ; } ; } /** Defines an upper bound of permissions on a particular resource. */ interface AccessBoundaryRule { availablePermissions : string [ ] ; availableResource : string ; availabilityCondition ? : AvailabilityCondition ; } /**
 * An optional condition that can be used as part of a
 * CredentialAccessBoundary to further restrict permissions.
 */ interface AvailabilityCondition { expression : string ; title ? : string ; description ? : string ; } /**
 * Defines a set of Google credentials that are downscoped from an existing set
 * of Google OAuth2 credentials. This is useful to restrict the Identity and
 * Access Management (IAM) permissions that a short-lived credential can use.
 * The common pattern of usage is to have a token broker with elevated access
 * generate these downscoped credentials from higher access source credentials
 * and pass the downscoped short-lived access tokens to a token consumer via
 * some secure authenticated channel for limited access to Google Cloud Storage
 * resources.
 */ export class DownscopedClient extends AuthClient { private cachedDownscopedAccessToken : CredentialsWithResponse | null ; private readonly stsCredential : sts . StsCredentials ; public readonly eagerRefreshThresholdMillis : number ; public readonly forceRefreshOnFailure : boolean ; param authClient param credentialAccessBoundary param additionalOptions param quotaProjectId constructor ( private readonly authClient , private readonly credentialAccessBoundary , additionalOptions ? , quotaProjectId ? ) { super ( ) ; if ( credentialAccessBoundary . accessBoundary . accessBoundaryRules . length === 0 ) { throw new Error ( 'At least one access boundary rule needs to be defined.' ) ; } else if ( credentialAccessBoundary . accessBoundary . accessBoundaryRules . length > MAX_ACCESS_BOUNDARY_RULES_COUNT ) { throw new Error ( 'The provided access boundary has more than ' + `${ MAX_ACCESS_BOUNDARY_RULES_COUNT } access boundary rules.` ) ; } for ( const rule of credentialAccessBoundary . accessBoundary . accessBoundaryRules ) { if ( rule . availablePermissions . length === 0 ) { throw new Error ( 'At least one permission should be defined in access boundary rules.' ) ; } } this . stsCredential = new sts . StsCredentials ( STS_ACCESS_TOKEN_URL ) ; this . cachedDownscopedAccessToken = null ; if ( typeof additionalOptions ?. eagerRefreshThresholdMillis !== 'number' ) { this . eagerRefreshThresholdMillis = EXPIRATION_TIME_OFFSET ; } else { this . eagerRefreshThresholdMillis = additionalOptions ! . eagerRefreshThresholdMillis as number ; } this . forceRefreshOnFailure = ! ! additionalOptions ?. forceRefreshOnFailure ; this . quotaProjectId = quotaProjectId ; } param credentials setCredentials ( credentials ) { if ( ! credentials . expiry_date ) { throw new Error ( 'The access token expiry_date field is missing in the provided ' + 'credentials.' ) ; } super . setCredentials ( credentials ) ; this . cachedDownscopedAccessToken = credentials ; } async getAccessToken ( ) { if ( ! this . cachedDownscopedAccessToken || this . isExpired ( this . cachedDownscopedAccessToken ) ) { await this . refreshAccessTokenAsync ( ) ; } return { token : this . cachedDownscopedAccessToken ! . access_token , expirationTime : this . cachedDownscopedAccessToken ! . expiry_date , res : this . cachedDownscopedAccessToken ! . 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 ) ; } } 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 . authClient . getAccessToken ( ) ) . token ; const stsCredentialsOptions = { grantType : STS_GRANT_TYPE , requestedTokenType : STS_REQUEST_TOKEN_TYPE , subjectToken : subjectToken as string , subjectTokenType : STS_SUBJECT_TOKEN_TYPE , } ; const stsResponse = await this . stsCredential . exchangeToken ( stsCredentialsOptions , undefined , this . credentialAccessBoundary ) ; /**
         * The STS endpoint will only return the expiration time for the downscoped
         * access token if the original access token represents a service account.
         * The downscoped token's expiration time will always match the source
         * credential expiration. When no expires_in is returned, we can copy the
         * source credential's expiration time.
         */ const sourceCredExpireDate = this . authClient . credentials ?. expiry_date || null ; const expiryDate = stsResponse . expires_in ? new Date ( ) . getTime ( ) + stsResponse . expires_in * 1000 : sourceCredExpireDate ; this . cachedDownscopedAccessToken = { access_token : stsResponse . access_token , expiry_date : expiryDate , res : stsResponse . res , } ; this . credentials = { } ; Object . assign ( this . credentials , this . cachedDownscopedAccessToken ) ; delete ( this . credentials as CredentialsWithResponse ) . res ; this . emit ( 'tokens' , { refresh_token : null , expiry_date : this . cachedDownscopedAccessToken ! . expiry_date , access_token : this . cachedDownscopedAccessToken ! . access_token , token_type : 'Bearer' , id_token : null , } ) ; return this . cachedDownscopedAccessToken ; } param downscopedAccessToken return private isExpired ( downscopedAccessToken ) { const now = new Date ( ) . getTime ( ) ; return downscopedAccessToken . expiry_date ? now >= downscopedAccessToken . expiry_date - this . eagerRefreshThresholdMillis : false ; } }