Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 47 additions & 2 deletions src/auth/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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,
);
}
}
37 changes: 34 additions & 3 deletions test/unit/auth/credential.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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;
Expand Down Expand Up @@ -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', () => {
Expand Down