Skip to content

Commit d52d133

Browse files
yinzarahiranya911
authored andcommitted
Handle special case of application default credentials location (#444)
* Handle special case of application default credentials When the GOOGLE_APPLICATION_CREDENTIALS environment variable is pointed to the refresh token file created by 'gcloud auth application-default login', Firebase admin would error as it tried to parse it as a certificate. This fix doesn't attempt to parse the file as a certificate if the variable points to the refresh token file and instead just attempts refresh token file parsing * update auth cert parsing based on review * fix credential parsing based on code review * fix test case and add to changelog
1 parent 026748d commit d52d133

File tree

3 files changed

+83
-5
lines changed

3 files changed

+83
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
- [fixed] Implemented a Node.js environment check that will be executed at
44
package import time.
5+
- [fixed] Setting GOOGLE_APPLICATION_CREDENTIALS environment variable
6+
to a refresh token instead of a certificate token now supported
57

68
# v6.5.0
79

src/auth/credential.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,7 @@ export class ApplicationDefaultCredential implements Credential {
356356

357357
constructor(httpAgent?: Agent) {
358358
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
359-
const serviceAccount = Certificate.fromPath(process.env.GOOGLE_APPLICATION_CREDENTIALS);
360-
this.credential_ = new CertCredential(serviceAccount, httpAgent);
359+
this.credential_ = credentialFromFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, httpAgent);
361360
return;
362361
}
363362

@@ -384,3 +383,49 @@ export class ApplicationDefaultCredential implements Credential {
384383
return this.credential_;
385384
}
386385
}
386+
387+
function credentialFromFile(filePath: string, httpAgent?: Agent): Credential {
388+
const credentialsFile = readCredentialFile(filePath);
389+
if (typeof credentialsFile !== 'object') {
390+
throw new FirebaseAppError(
391+
AppErrorCodes.INVALID_CREDENTIAL,
392+
'Failed to parse contents of the credentials file as an object',
393+
);
394+
}
395+
if (credentialsFile.type === 'service_account') {
396+
return new CertCredential(credentialsFile, httpAgent);
397+
}
398+
if (credentialsFile.type === 'authorized_user') {
399+
return new RefreshTokenCredential(credentialsFile, httpAgent);
400+
}
401+
throw new FirebaseAppError(
402+
AppErrorCodes.INVALID_CREDENTIAL,
403+
'Invalid contents in the credentials file',
404+
);
405+
}
406+
407+
function readCredentialFile(filePath: string): {[key: string]: any} {
408+
if (typeof filePath !== 'string') {
409+
throw new FirebaseAppError(
410+
AppErrorCodes.INVALID_CREDENTIAL,
411+
'Failed to parse credentials file: TypeError: path must be a string',
412+
);
413+
}
414+
let fileText: string;
415+
try {
416+
fileText = fs.readFileSync(filePath, 'utf8');
417+
} catch (error) {
418+
throw new FirebaseAppError(
419+
AppErrorCodes.INVALID_CREDENTIAL,
420+
`Failed to read credentials from file ${filePath}: ` + error,
421+
);
422+
}
423+
try {
424+
return JSON.parse(fileText);
425+
} catch (error) {
426+
throw new FirebaseAppError(
427+
AppErrorCodes.INVALID_CREDENTIAL,
428+
'Failed to parse contents of the credentials file as an object: ' + error,
429+
);
430+
}
431+
}

test/unit/auth/credential.spec.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ import * as utils from '../utils';
3131
import * as mocks from '../../resources/mocks';
3232

3333
import {
34-
ApplicationDefaultCredential, CertCredential, Certificate, GoogleOAuthAccessToken,
35-
MetadataServiceCredential, RefreshTokenCredential,
34+
ApplicationDefaultCredential, CertCredential, Certificate, Credential, GoogleOAuthAccessToken,
35+
MetadataServiceCredential, RefreshToken, RefreshTokenCredential,
3636
} from '../../../src/auth/credential';
3737
import { HttpClient } from '../../../src/utils/api-request';
3838
import {Agent} from 'https';
@@ -337,7 +337,7 @@ describe('Credential', () => {
337337
if (fsStub) {
338338
fsStub.restore();
339339
}
340-
process.env.GOOGLE_APPLICATION_CREDENTIALS = this.credPath;
340+
process.env.GOOGLE_APPLICATION_CREDENTIALS = credPath;
341341
});
342342

343343
it('should return a CertCredential with GOOGLE_APPLICATION_CREDENTIALS set', () => {
@@ -356,6 +356,19 @@ describe('Credential', () => {
356356
expect(() => new ApplicationDefaultCredential()).to.throw(Error);
357357
});
358358

359+
it('should throw error if type not specified on cert file', () => {
360+
fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify({}));
361+
expect(() => new ApplicationDefaultCredential())
362+
.to.throw(Error, 'Invalid contents in the credentials file');
363+
});
364+
365+
it('should throw error if type is unknown on cert file', () => {
366+
fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify({
367+
type: 'foo',
368+
}));
369+
expect(() => new ApplicationDefaultCredential()).to.throw(Error, 'Invalid contents in the credentials file');
370+
});
371+
359372
it('should return a RefreshTokenCredential with gcloud login', () => {
360373
if (skipAndLogWarningIfNoGcloud()) {
361374
return;
@@ -395,6 +408,24 @@ describe('Credential', () => {
395408
privateKey: mockCertificateObject.private_key,
396409
});
397410
});
411+
412+
it('should parse valid RefreshTokenCredential if GOOGLE_APPLICATION_CREDENTIALS environment variable ' +
413+
'points to default refresh token location', () => {
414+
process.env.GOOGLE_APPLICATION_CREDENTIALS = GCLOUD_CREDENTIAL_PATH;
415+
416+
fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify(MOCK_REFRESH_TOKEN_CONFIG));
417+
418+
const adc = new ApplicationDefaultCredential();
419+
const c = adc.getCredential();
420+
expect(c).is.instanceOf(RefreshTokenCredential);
421+
expect(c).to.have.property('refreshToken').that.includes({
422+
clientId: MOCK_REFRESH_TOKEN_CONFIG.client_id,
423+
clientSecret: MOCK_REFRESH_TOKEN_CONFIG.client_secret,
424+
refreshToken: MOCK_REFRESH_TOKEN_CONFIG.refresh_token,
425+
type: MOCK_REFRESH_TOKEN_CONFIG.type,
426+
});
427+
expect(fsStub.alwaysCalledWith(GCLOUD_CREDENTIAL_PATH, 'utf8')).to.be.true;
428+
});
398429
});
399430

400431
describe('HTTP Agent', () => {

0 commit comments

Comments
 (0)