Skip to content

Commit 7dbf080

Browse files
authored
Fixing error handling in Firebase credentials (#448)
* Fixing error handling in Firebase credentials * Added comments * Updated changelog
1 parent 2952450 commit 7dbf080

File tree

4 files changed

+84
-17
lines changed

4 files changed

+84
-17
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

3-
-
3+
- [fixed] Including additional helpful details in the errors thrown due to
4+
credentials-related problems.
45

56
# v6.5.1
67

src/auth/credential.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import os = require('os');
2020
import path = require('path');
2121

2222
import {AppErrorCodes, FirebaseAppError} from '../utils/error';
23-
import {HttpClient, HttpRequestConfig} from '../utils/api-request';
23+
import {HttpClient, HttpRequestConfig, HttpError, HttpResponse} from '../utils/api-request';
2424
import {Agent} from 'http';
2525

2626
const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token';
@@ -190,28 +190,43 @@ export interface GoogleOAuthAccessToken {
190190
function requestAccessToken(client: HttpClient, request: HttpRequestConfig): Promise<GoogleOAuthAccessToken> {
191191
return client.send(request).then((resp) => {
192192
const json = resp.data;
193-
if (json.error) {
194-
let errorMessage = 'Error fetching access token: ' + json.error;
195-
if (json.error_description) {
196-
errorMessage += ' (' + json.error_description + ')';
197-
}
198-
throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage);
199-
} else if (!json.access_token || !json.expires_in) {
193+
if (!json.access_token || !json.expires_in) {
200194
throw new FirebaseAppError(
201195
AppErrorCodes.INVALID_CREDENTIAL,
202196
`Unexpected response while fetching access token: ${ JSON.stringify(json) }`,
203197
);
204-
} else {
205-
return json;
206198
}
199+
return json;
207200
}).catch((err) => {
208-
throw new FirebaseAppError(
209-
AppErrorCodes.INVALID_CREDENTIAL,
210-
`Failed to parse access token response: ${err.toString()}`,
211-
);
201+
throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err));
212202
});
213203
}
214204

205+
/**
206+
* Constructs a human-readable error message from the given Error.
207+
*/
208+
function getErrorMessage(err: Error): string {
209+
const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message;
210+
return `Error fetching access token: ${detail}`;
211+
}
212+
213+
/**
214+
* Extracts details from the given HTTP error response, and returns a human-readable description. If
215+
* the response is JSON-formatted, looks up the error and error_description fields sent by the
216+
* Google Auth servers. Otherwise returns the entire response payload as the error detail.
217+
*/
218+
function getDetailFromResponse(response: HttpResponse): string {
219+
if (response.isJson() && response.data.error) {
220+
const json = response.data;
221+
let detail = json.error;
222+
if (json.error_description) {
223+
detail += ' (' + json.error_description + ')';
224+
}
225+
return detail;
226+
}
227+
return response.text;
228+
}
229+
215230
/**
216231
* Implementation of Credential that uses a service account certificate.
217232
*/

test/unit/auth/credential.spec.ts

Lines changed: 27 additions & 2 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, Credential, GoogleOAuthAccessToken,
35-
MetadataServiceCredential, RefreshToken, RefreshTokenCredential,
34+
ApplicationDefaultCredential, CertCredential, Certificate, GoogleOAuthAccessToken,
35+
MetadataServiceCredential, RefreshTokenCredential,
3636
} from '../../../src/auth/credential';
3737
import { HttpClient } from '../../../src/utils/api-request';
3838
import {Agent} from 'https';
@@ -262,6 +262,31 @@ describe('Credential', () => {
262262
expect(token.expires_in).to.equal(ONE_HOUR_IN_SECONDS);
263263
});
264264
});
265+
266+
describe('Error Handling', () => {
267+
let httpStub: sinon.SinonStub;
268+
before(() => {
269+
httpStub = sinon.stub(HttpClient.prototype, 'send');
270+
});
271+
after(() => httpStub.restore());
272+
273+
it('should throw an error including error details', () => {
274+
httpStub.rejects(utils.errorFrom({
275+
error: 'invalid_grant',
276+
error_description: 'reason',
277+
}));
278+
const c = new CertCredential(mockCertificateObject);
279+
return expect(c.getAccessToken()).to.be
280+
.rejectedWith('Error fetching access token: invalid_grant (reason)');
281+
});
282+
283+
it('should throw an error including error text payload', () => {
284+
httpStub.rejects(utils.errorFrom('not json'));
285+
const c = new CertCredential(mockCertificateObject);
286+
return expect(c.getAccessToken()).to.be
287+
.rejectedWith('Error fetching access token: not json');
288+
});
289+
});
265290
});
266291

267292
describe('RefreshTokenCredential', () => {

test/unit/firebase-app.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {Firestore} from '@google-cloud/firestore';
4040
import {Database} from '@firebase/database';
4141
import {InstanceId} from '../../src/instance-id/instance-id';
4242
import {ProjectManagement} from '../../src/project-management/project-management';
43+
import { FirebaseAppError, AppErrorCodes } from '../../src/utils/error';
4344

4445
chai.should();
4546
chai.use(sinonChai);
@@ -910,6 +911,31 @@ describe('FirebaseApp', () => {
910911
});
911912
});
912913
});
914+
915+
it('Includes the original error in exception', () => {
916+
getTokenStub.restore();
917+
const mockError = new FirebaseAppError(
918+
AppErrorCodes.INVALID_CREDENTIAL, 'Something went wrong');
919+
getTokenStub = sinon.stub(CertCredential.prototype, 'getAccessToken').rejects(mockError);
920+
const detailedMessage = 'Credential implementation provided to initializeApp() via the "credential" property'
921+
+ ' failed to fetch a valid Google OAuth2 access token with the following error: "Something went wrong".';
922+
expect(mockApp.INTERNAL.getToken(true)).to.be.rejectedWith(detailedMessage);
923+
});
924+
925+
it('Returns a detailed message when an error is due to an invalid_grant', () => {
926+
getTokenStub.restore();
927+
const mockError = new FirebaseAppError(
928+
AppErrorCodes.INVALID_CREDENTIAL, 'Failed to get credentials: invalid_grant (reason)');
929+
getTokenStub = sinon.stub(CertCredential.prototype, 'getAccessToken').rejects(mockError);
930+
const detailedMessage = 'Credential implementation provided to initializeApp() via the "credential" property'
931+
+ ' failed to fetch a valid Google OAuth2 access token with the following error: "Failed to get credentials:'
932+
+ ' invalid_grant (reason)". There are two likely causes: (1) your server time is not properly synced or (2)'
933+
+ ' your certificate key file has been revoked. To solve (1), re-sync the time on your server. To solve (2),'
934+
+ ' make sure the key ID for your key file is still present at '
935+
+ 'https://console.firebase.google.com/iam-admin/serviceaccounts/project. If not, generate a new key file '
936+
+ 'at https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.';
937+
expect(mockApp.INTERNAL.getToken(true)).to.be.rejectedWith(detailedMessage);
938+
});
913939
});
914940

915941
describe('INTERNAL.addAuthTokenListener()', () => {

0 commit comments

Comments
 (0)