@@ -366,6 +366,107 @@ class RefreshToken {
366
366
}
367
367
368
368
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
+
369
470
/**
370
471
* Checks if the given credential was loaded via the application default credentials mechanism. This
371
472
* includes all ComputeEngineCredential instances, and the ServiceAccountCredential and RefreshTokenCredential
@@ -377,20 +478,19 @@ class RefreshToken {
377
478
export function isApplicationDefault ( credential ?: Credential ) : boolean {
378
479
return credential instanceof ComputeEngineCredential ||
379
480
( credential instanceof ServiceAccountCredential && credential . implicit ) ||
380
- ( credential instanceof RefreshTokenCredential && credential . implicit ) ;
481
+ ( credential instanceof RefreshTokenCredential && credential . implicit ) ||
482
+ ( credential instanceof ImpersonatedServiceAccountCredential && credential . implicit ) ;
381
483
}
382
484
383
485
export function getApplicationDefault ( httpAgent ?: Agent ) : Credential {
384
486
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 ) ! ;
386
488
}
387
489
388
490
// It is OK to not have this file. If it is present, it must be valid.
389
491
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
394
494
}
395
495
396
496
return new ComputeEngineCredential ( httpAgent ) ;
@@ -474,9 +574,10 @@ function getDetailFromResponse(response: HttpResponse): string {
474
574
return response . text || 'Missing error payload' ;
475
575
}
476
576
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 ) ;
479
579
if ( typeof credentialsFile !== 'object' || credentialsFile === null ) {
580
+ if ( ignoreMissing ) { return null ; }
480
581
throw new FirebaseAppError (
481
582
AppErrorCodes . INVALID_CREDENTIAL ,
482
583
'Failed to parse contents of the credentials file as an object' ,
@@ -491,6 +592,10 @@ function credentialFromFile(filePath: string, httpAgent?: Agent): Credential {
491
592
return new RefreshTokenCredential ( credentialsFile , httpAgent , true ) ;
492
593
}
493
594
595
+ if ( credentialsFile . type === 'impersonated_service_account' ) {
596
+ return new ImpersonatedServiceAccountCredential ( credentialsFile , httpAgent , true )
597
+ }
598
+
494
599
throw new FirebaseAppError (
495
600
AppErrorCodes . INVALID_CREDENTIAL ,
496
601
'Invalid contents in the credentials file' ,
0 commit comments