diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c1d5769e..039bf2e597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ - [fixed] Implemented a Node.js environment check that will be executed at package import time. +- [fixed] Setting GOOGLE_APPLICATION_CREDENTIALS environment variable + to a refresh token instead of a certificate token now supported # v6.5.0 diff --git a/src/auth/credential.ts b/src/auth/credential.ts index e178ca8f10..fad788b7d3 100644 --- a/src/auth/credential.ts +++ b/src/auth/credential.ts @@ -356,8 +356,7 @@ export class ApplicationDefaultCredential implements Credential { constructor(httpAgent?: Agent) { if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { - const serviceAccount = Certificate.fromPath(process.env.GOOGLE_APPLICATION_CREDENTIALS); - this.credential_ = new CertCredential(serviceAccount, httpAgent); + this.credential_ = credentialFromFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, httpAgent); return; } @@ -384,3 +383,49 @@ export class ApplicationDefaultCredential implements Credential { return this.credential_; } } + +function credentialFromFile(filePath: string, httpAgent?: Agent): Credential { + const credentialsFile = readCredentialFile(filePath); + if (typeof credentialsFile !== 'object') { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Failed to parse contents of the credentials file as an object', + ); + } + if (credentialsFile.type === 'service_account') { + return new CertCredential(credentialsFile, httpAgent); + } + if (credentialsFile.type === 'authorized_user') { + return new RefreshTokenCredential(credentialsFile, httpAgent); + } + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Invalid contents in the credentials file', + ); +} + +function readCredentialFile(filePath: string): {[key: string]: any} { + if (typeof filePath !== 'string') { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Failed to parse credentials file: TypeError: path must be a string', + ); + } + let fileText: string; + try { + fileText = fs.readFileSync(filePath, 'utf8'); + } catch (error) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + `Failed to read credentials from file ${filePath}: ` + error, + ); + } + try { + return JSON.parse(fileText); + } catch (error) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Failed to parse contents of the credentials file as an object: ' + error, + ); + } +} diff --git a/test/unit/auth/credential.spec.ts b/test/unit/auth/credential.spec.ts index 27fdf7cb48..e902f65da2 100644 --- a/test/unit/auth/credential.spec.ts +++ b/test/unit/auth/credential.spec.ts @@ -31,8 +31,8 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { - ApplicationDefaultCredential, CertCredential, Certificate, GoogleOAuthAccessToken, - MetadataServiceCredential, RefreshTokenCredential, + ApplicationDefaultCredential, CertCredential, Certificate, Credential, GoogleOAuthAccessToken, + MetadataServiceCredential, RefreshToken, RefreshTokenCredential, } from '../../../src/auth/credential'; import { HttpClient } from '../../../src/utils/api-request'; import {Agent} from 'https'; @@ -337,7 +337,7 @@ describe('Credential', () => { if (fsStub) { fsStub.restore(); } - process.env.GOOGLE_APPLICATION_CREDENTIALS = this.credPath; + process.env.GOOGLE_APPLICATION_CREDENTIALS = credPath; }); it('should return a CertCredential with GOOGLE_APPLICATION_CREDENTIALS set', () => { @@ -356,6 +356,19 @@ describe('Credential', () => { expect(() => new ApplicationDefaultCredential()).to.throw(Error); }); + it('should throw error if type not specified on cert file', () => { + fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify({})); + expect(() => new ApplicationDefaultCredential()) + .to.throw(Error, 'Invalid contents in the credentials file'); + }); + + it('should throw error if type is unknown on cert file', () => { + fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify({ + type: 'foo', + })); + expect(() => new ApplicationDefaultCredential()).to.throw(Error, 'Invalid contents in the credentials file'); + }); + it('should return a RefreshTokenCredential with gcloud login', () => { if (skipAndLogWarningIfNoGcloud()) { return; @@ -395,6 +408,24 @@ describe('Credential', () => { privateKey: mockCertificateObject.private_key, }); }); + + it('should parse valid RefreshTokenCredential if GOOGLE_APPLICATION_CREDENTIALS environment variable ' + + 'points to default refresh token location', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = GCLOUD_CREDENTIAL_PATH; + + fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify(MOCK_REFRESH_TOKEN_CONFIG)); + + const adc = new ApplicationDefaultCredential(); + const c = adc.getCredential(); + expect(c).is.instanceOf(RefreshTokenCredential); + expect(c).to.have.property('refreshToken').that.includes({ + clientId: MOCK_REFRESH_TOKEN_CONFIG.client_id, + clientSecret: MOCK_REFRESH_TOKEN_CONFIG.client_secret, + refreshToken: MOCK_REFRESH_TOKEN_CONFIG.refresh_token, + type: MOCK_REFRESH_TOKEN_CONFIG.type, + }); + expect(fsStub.alwaysCalledWith(GCLOUD_CREDENTIAL_PATH, 'utf8')).to.be.true; + }); }); describe('HTTP Agent', () => {