@@ -366,6 +366,107 @@ class RefreshToken {
366366}
367367
368368
369+ /**
370+ * Implementation of Credential that uses impersonated service account.
371+ */
372+ export class ImpersonatedServiceAccountCredential implements Credential {
373+
374+ private readonly impersonatedServiceAccount : ImpersonatedServiceAccount ;
375+ private readonly httpClient : HttpClient ;
376+
377+ /**
378+ * Creates a new ImpersonatedServiceAccountCredential from the given parameters.
379+ *
380+ * @param impersonatedServiceAccountPathOrObject - Impersonated Service account json object or
381+ * path to a service account json file.
382+ * @param httpAgent - Optional http.Agent to use when calling the remote token server.
383+ * @param implicit - An optinal boolean indicating whether this credential was implicitly
384+ * discovered from the environment, as opposed to being explicitly specified by the developer.
385+ *
386+ * @constructor
387+ */
388+ constructor (
389+ impersonatedServiceAccountPathOrObject : string | object ,
390+ private readonly httpAgent ?: Agent ,
391+ readonly implicit : boolean = false ) {
392+
393+ this . impersonatedServiceAccount = ( typeof impersonatedServiceAccountPathOrObject === 'string' ) ?
394+ ImpersonatedServiceAccount . fromPath ( impersonatedServiceAccountPathOrObject )
395+ : new ImpersonatedServiceAccount ( impersonatedServiceAccountPathOrObject ) ;
396+ this . httpClient = new HttpClient ( ) ;
397+ }
398+
399+ public getAccessToken ( ) : Promise < GoogleOAuthAccessToken > {
400+ const postData =
401+ 'client_id=' + this . impersonatedServiceAccount . clientId + '&' +
402+ 'client_secret=' + this . impersonatedServiceAccount . clientSecret + '&' +
403+ 'refresh_token=' + this . impersonatedServiceAccount . refreshToken + '&' +
404+ 'grant_type=refresh_token' ;
405+ const request : HttpRequestConfig = {
406+ method : 'POST' ,
407+ url : `https://${ REFRESH_TOKEN_HOST } ${ REFRESH_TOKEN_PATH } ` ,
408+ headers : {
409+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
410+ } ,
411+ data : postData ,
412+ httpAgent : this . httpAgent ,
413+ } ;
414+ return requestAccessToken ( this . httpClient , request ) ;
415+ }
416+ }
417+
418+ /**
419+ * A struct containing the properties necessary to use impersonated service account JSON credentials.
420+ */
421+ class ImpersonatedServiceAccount {
422+
423+ public readonly clientId : string ;
424+ public readonly clientSecret : string ;
425+ public readonly refreshToken : string ;
426+ public readonly type : string ;
427+
428+ /*
429+ * Tries to load a ImpersonatedServiceAccount from a path. Throws if the path doesn't exist or the
430+ * data at the path is invalid.
431+ */
432+ public static fromPath ( filePath : string ) : ImpersonatedServiceAccount {
433+ try {
434+ return new ImpersonatedServiceAccount ( JSON . parse ( fs . readFileSync ( filePath , 'utf8' ) ) ) ;
435+ } catch ( error ) {
436+ // Throw a nicely formed error message if the file contents cannot be parsed
437+ throw new FirebaseAppError (
438+ AppErrorCodes . INVALID_CREDENTIAL ,
439+ 'Failed to parse impersonated service account file: ' + error ,
440+ ) ;
441+ }
442+ }
443+
444+ constructor ( json : object ) {
445+ const sourceCredentials = ( json as { [ key : string ] : any } ) [ 'source_credentials' ]
446+ if ( sourceCredentials ) {
447+ copyAttr ( this , sourceCredentials , 'clientId' , 'client_id' ) ;
448+ copyAttr ( this , sourceCredentials , 'clientSecret' , 'client_secret' ) ;
449+ copyAttr ( this , sourceCredentials , 'refreshToken' , 'refresh_token' ) ;
450+ copyAttr ( this , sourceCredentials , 'type' , 'type' ) ;
451+ }
452+
453+ let errorMessage ;
454+ if ( ! util . isNonEmptyString ( this . clientId ) ) {
455+ errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_id" property.' ;
456+ } else if ( ! util . isNonEmptyString ( this . clientSecret ) ) {
457+ errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_secret" property.' ;
458+ } else if ( ! util . isNonEmptyString ( this . refreshToken ) ) {
459+ errorMessage = 'Impersonated Service Account must contain a "source_credentials.refresh_token" property.' ;
460+ } else if ( ! util . isNonEmptyString ( this . type ) ) {
461+ errorMessage = 'Impersonated Service Account must contain a "source_credentials.type" property.' ;
462+ }
463+
464+ if ( typeof errorMessage !== 'undefined' ) {
465+ throw new FirebaseAppError ( AppErrorCodes . INVALID_CREDENTIAL , errorMessage ) ;
466+ }
467+ }
468+ }
469+
369470/**
370471 * Checks if the given credential was loaded via the application default credentials mechanism. This
371472 * includes all ComputeEngineCredential instances, and the ServiceAccountCredential and RefreshTokenCredential
@@ -377,20 +478,19 @@ class RefreshToken {
377478export function isApplicationDefault ( credential ?: Credential ) : boolean {
378479 return credential instanceof ComputeEngineCredential ||
379480 ( credential instanceof ServiceAccountCredential && credential . implicit ) ||
380- ( credential instanceof RefreshTokenCredential && credential . implicit ) ;
481+ ( credential instanceof RefreshTokenCredential && credential . implicit ) ||
482+ ( credential instanceof ImpersonatedServiceAccountCredential && credential . implicit ) ;
381483}
382484
383485export function getApplicationDefault ( httpAgent ?: Agent ) : Credential {
384486 if ( process . env . GOOGLE_APPLICATION_CREDENTIALS ) {
385- return credentialFromFile ( process . env . GOOGLE_APPLICATION_CREDENTIALS , httpAgent ) ;
487+ return credentialFromFile ( process . env . GOOGLE_APPLICATION_CREDENTIALS , httpAgent , false ) ! ;
386488 }
387489
388490 // It is OK to not have this file. If it is present, it must be valid.
389491 if ( GCLOUD_CREDENTIAL_PATH ) {
390- const refreshToken = readCredentialFile ( GCLOUD_CREDENTIAL_PATH , true ) ;
391- if ( refreshToken ) {
392- return new RefreshTokenCredential ( refreshToken , httpAgent , true ) ;
393- }
492+ const credential = credentialFromFile ( GCLOUD_CREDENTIAL_PATH , httpAgent , true ) ;
493+ if ( credential ) return credential
394494 }
395495
396496 return new ComputeEngineCredential ( httpAgent ) ;
@@ -474,9 +574,10 @@ function getDetailFromResponse(response: HttpResponse): string {
474574 return response . text || 'Missing error payload' ;
475575}
476576
477- function credentialFromFile ( filePath : string , httpAgent ?: Agent ) : Credential {
478- const credentialsFile = readCredentialFile ( filePath ) ;
577+ function credentialFromFile ( filePath : string , httpAgent ?: Agent , ignoreMissing ?: boolean ) : Credential | null {
578+ const credentialsFile = readCredentialFile ( filePath , ignoreMissing ) ;
479579 if ( typeof credentialsFile !== 'object' || credentialsFile === null ) {
580+ if ( ignoreMissing ) { return null ; }
480581 throw new FirebaseAppError (
481582 AppErrorCodes . INVALID_CREDENTIAL ,
482583 'Failed to parse contents of the credentials file as an object' ,
@@ -491,6 +592,10 @@ function credentialFromFile(filePath: string, httpAgent?: Agent): Credential {
491592 return new RefreshTokenCredential ( credentialsFile , httpAgent , true ) ;
492593 }
493594
595+ if ( credentialsFile . type === 'impersonated_service_account' ) {
596+ return new ImpersonatedServiceAccountCredential ( credentialsFile , httpAgent , true )
597+ }
598+
494599 throw new FirebaseAppError (
495600 AppErrorCodes . INVALID_CREDENTIAL ,
496601 'Invalid contents in the credentials file' ,
0 commit comments