Skip to content

Commit e953b34

Browse files
authored
Refactored the credentials implementation (#730)
* Updated unit tests * Further cleaned up the credential impl * Updated comments * Added more tests * Fixed test for GCP environment * Fixed failing IAMSigner test * Added some docs; Improved validation logic
1 parent f66c90a commit e953b34

15 files changed

+470
-565
lines changed

src/auth/credential.ts

Lines changed: 231 additions & 274 deletions
Large diffs are not rendered by default.

src/auth/token-generator.ts

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { FirebaseApp } from '../firebase-app';
18-
import {Certificate, tryGetCertificate} from './credential';
18+
import {ServiceAccountCredential} from './credential';
1919
import {AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
2020
import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '../utils/api-request';
2121

@@ -82,28 +82,19 @@ interface JWTBody {
8282
* sign data. Performs all operations locally, and does not make any RPC calls.
8383
*/
8484
export class ServiceAccountSigner implements CryptoSigner {
85-
private readonly certificate: Certificate;
8685

8786
/**
88-
* Creates a new CryptoSigner instance from the given service account certificate.
87+
* Creates a new CryptoSigner instance from the given service account credential.
8988
*
90-
* @param {Certificate} certificate A service account certificate.
89+
* @param {ServiceAccountCredential} credential A service account credential.
9190
*/
92-
constructor(certificate: Certificate) {
93-
if (!certificate) {
91+
constructor(private readonly credential: ServiceAccountCredential) {
92+
if (!credential) {
9493
throw new FirebaseAuthError(
9594
AuthClientErrorCode.INVALID_CREDENTIAL,
96-
'INTERNAL ASSERT: Must provide a certificate to initialize ServiceAccountSigner.',
95+
'INTERNAL ASSERT: Must provide a service account credential to initialize ServiceAccountSigner.',
9796
);
9897
}
99-
if (!validator.isNonEmptyString(certificate.clientEmail) || !validator.isNonEmptyString(certificate.privateKey)) {
100-
throw new FirebaseAuthError(
101-
AuthClientErrorCode.INVALID_CREDENTIAL,
102-
'INTERNAL ASSERT: Must provide a certificate with validate clientEmail and privateKey to ' +
103-
'initialize ServiceAccountSigner.',
104-
);
105-
}
106-
this.certificate = certificate;
10798
}
10899

109100
/**
@@ -113,14 +104,14 @@ export class ServiceAccountSigner implements CryptoSigner {
113104
const crypto = require('crypto');
114105
const sign = crypto.createSign('RSA-SHA256');
115106
sign.update(buffer);
116-
return Promise.resolve(sign.sign(this.certificate.privateKey));
107+
return Promise.resolve(sign.sign(this.credential.privateKey));
117108
}
118109

119110
/**
120111
* @inheritDoc
121112
*/
122113
public getAccountId(): Promise<string> {
123-
return Promise.resolve(this.certificate.clientEmail);
114+
return Promise.resolve(this.credential.clientEmail);
124115
}
125116
}
126117

@@ -232,12 +223,11 @@ export class IAMSigner implements CryptoSigner {
232223
* @return {CryptoSigner} A CryptoSigner instance.
233224
*/
234225
export function cryptoSignerFromApp(app: FirebaseApp): CryptoSigner {
235-
if (app.options.credential) {
236-
const cert = tryGetCertificate(app.options.credential);
237-
if (cert != null && validator.isNonEmptyString(cert.privateKey) && validator.isNonEmptyString(cert.clientEmail)) {
238-
return new ServiceAccountSigner(cert);
239-
}
226+
const credential = app.options.credential;
227+
if (credential instanceof ServiceAccountCredential) {
228+
return new ServiceAccountSigner(credential);
240229
}
230+
241231
return new IAMSigner(new AuthorizedHttpClient(app), app.options.serviceAccountId);
242232
}
243233

src/firebase-app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {ApplicationDefaultCredential, Credential, GoogleOAuthAccessToken} from './auth/credential';
17+
import {Credential, GoogleOAuthAccessToken, getApplicationDefault} from './auth/credential';
1818
import * as validator from './utils/validator';
1919
import {deepCopy, deepExtend} from './utils/deep-copy';
2020
import {FirebaseServiceInterface} from './firebase-service';
@@ -264,7 +264,7 @@ export class FirebaseApp {
264264

265265
const hasCredential = ('credential' in this.options_);
266266
if (!hasCredential) {
267-
this.options_.credential = new ApplicationDefaultCredential();
267+
this.options_.credential = getApplicationDefault(this.options_.httpAgent);
268268
}
269269

270270
const credential = this.options_.credential;

src/firebase-namespace.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import {AppHook, FirebaseApp, FirebaseAppOptions} from './firebase-app';
2222
import {FirebaseServiceFactory, FirebaseServiceInterface} from './firebase-service';
2323
import {
2424
Credential,
25-
CertCredential,
2625
RefreshTokenCredential,
27-
ApplicationDefaultCredential,
26+
ServiceAccountCredential,
27+
getApplicationDefault,
2828
} from './auth/credential';
2929

3030
import {Auth} from './auth/auth';
@@ -48,8 +48,8 @@ const DEFAULT_APP_NAME = '[DEFAULT]';
4848
export const FIREBASE_CONFIG_VAR: string = 'FIREBASE_CONFIG';
4949

5050

51-
let globalAppDefaultCred: ApplicationDefaultCredential;
52-
const globalCertCreds: { [key: string]: CertCredential } = {};
51+
let globalAppDefaultCred: Credential;
52+
const globalCertCreds: { [key: string]: ServiceAccountCredential } = {};
5353
const globalRefreshTokenCreds: { [key: string]: RefreshTokenCredential } = {};
5454

5555

@@ -85,7 +85,7 @@ export class FirebaseNamespaceInternals {
8585
public initializeApp(options?: FirebaseAppOptions, appName = DEFAULT_APP_NAME): FirebaseApp {
8686
if (typeof options === 'undefined') {
8787
options = this.loadOptionsFromEnvVar();
88-
options.credential = new ApplicationDefaultCredential();
88+
options.credential = getApplicationDefault();
8989
}
9090
if (typeof appName !== 'string' || appName === '') {
9191
throw new FirebaseAppError(
@@ -275,7 +275,7 @@ const firebaseCredential = {
275275
cert: (serviceAccountPathOrObject: string | object, httpAgent?: Agent): Credential => {
276276
const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject);
277277
if (!(stringifiedServiceAccount in globalCertCreds)) {
278-
globalCertCreds[stringifiedServiceAccount] = new CertCredential(serviceAccountPathOrObject, httpAgent);
278+
globalCertCreds[stringifiedServiceAccount] = new ServiceAccountCredential(serviceAccountPathOrObject, httpAgent);
279279
}
280280
return globalCertCreds[stringifiedServiceAccount];
281281
},
@@ -291,7 +291,7 @@ const firebaseCredential = {
291291

292292
applicationDefault: (httpAgent?: Agent): Credential => {
293293
if (typeof globalAppDefaultCred === 'undefined') {
294-
globalAppDefaultCred = new ApplicationDefaultCredential(httpAgent);
294+
globalAppDefaultCred = getApplicationDefault(httpAgent);
295295
}
296296
return globalAppDefaultCred;
297297
},

src/firestore/firestore.ts

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import {FirebaseApp} from '../firebase-app';
1818
import {FirebaseFirestoreError} from '../utils/error';
1919
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
20-
import {ApplicationDefaultCredential, Certificate, tryGetCertificate} from '../auth/credential';
20+
import {ServiceAccountCredential, ComputeEngineCredential} from '../auth/credential';
2121
import {Firestore, Settings} from '@google-cloud/firestore';
2222

2323
import * as validator from '../utils/validator';
@@ -72,30 +72,20 @@ export function getFirestoreOptions(app: FirebaseApp): Settings {
7272
}
7373

7474
const projectId: string | null = utils.getProjectId(app);
75-
const cert: Certificate | null = tryGetCertificate(app.options.credential);
75+
const credential = app.options.credential;
7676
const { version: firebaseVersion } = require('../../package.json');
77-
if (cert != null) {
78-
// cert is available when the SDK has been initialized with a service account JSON file,
79-
// or by setting the GOOGLE_APPLICATION_CREDENTIALS envrionment variable.
80-
81-
if (!validator.isNonEmptyString(projectId)) {
82-
// Assert for an explicit projct ID (either via AppOptions or the cert itself).
83-
throw new FirebaseFirestoreError({
84-
code: 'no-project-id',
85-
message: 'Failed to determine project ID for Firestore. Initialize the '
86-
+ 'SDK with service account credentials or set project ID as an app option. '
87-
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
88-
});
89-
}
77+
if (credential instanceof ServiceAccountCredential) {
9078
return {
9179
credentials: {
92-
private_key: cert.privateKey,
93-
client_email: cert.clientEmail,
80+
private_key: credential.privateKey,
81+
client_email: credential.clientEmail,
9482
},
95-
projectId,
83+
// When the SDK is initialized with ServiceAccountCredentials projectId is guaranteed to
84+
// be available.
85+
projectId: projectId!,
9686
firebaseVersion,
9787
};
98-
} else if (app.options.credential instanceof ApplicationDefaultCredential) {
88+
} else if (app.options.credential instanceof ComputeEngineCredential) {
9989
// Try to use the Google application default credentials.
10090
// If an explicit project ID is not available, let Firestore client discover one from the
10191
// environment. This prevents the users from having to set GOOGLE_CLOUD_PROJECT in GCP runtimes.

src/storage/storage.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
import {FirebaseApp} from '../firebase-app';
1818
import {FirebaseError} from '../utils/error';
1919
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
20-
import {ApplicationDefaultCredential, Certificate, tryGetCertificate} from '../auth/credential';
20+
import {ServiceAccountCredential, ComputeEngineCredential} from '../auth/credential';
2121
import {Bucket, Storage as StorageClient} from '@google-cloud/storage';
2222

23+
import * as utils from '../utils/index';
2324
import * as validator from '../utils/validator';
2425

2526
/**
@@ -70,18 +71,19 @@ export class Storage implements FirebaseServiceInterface {
7071
});
7172
}
7273

73-
const cert: Certificate | null = tryGetCertificate(app.options.credential);
74-
if (cert != null) {
75-
// cert is available when the SDK has been initialized with a service account JSON file,
76-
// or by setting the GOOGLE_APPLICATION_CREDENTIALS envrionment variable.
74+
const projectId: string | null = utils.getProjectId(app);
75+
const credential = app.options.credential;
76+
if (credential instanceof ServiceAccountCredential) {
7777
this.storageClient = new storage({
78-
projectId: cert.projectId,
78+
// When the SDK is initialized with ServiceAccountCredentials projectId is guaranteed to
79+
// be available.
80+
projectId: projectId!,
7981
credentials: {
80-
private_key: cert.privateKey,
81-
client_email: cert.clientEmail,
82+
private_key: credential.privateKey,
83+
client_email: credential.clientEmail,
8284
},
8385
});
84-
} else if (app.options.credential instanceof ApplicationDefaultCredential) {
86+
} else if (app.options.credential instanceof ComputeEngineCredential) {
8587
// Try to use the Google application default credentials.
8688
this.storageClient = new storage();
8789
} else {

src/utils/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import {FirebaseApp, FirebaseAppOptions} from '../firebase-app';
18-
import {Certificate, tryGetCertificate} from '../auth/credential';
18+
import {ServiceAccountCredential} from '../auth/credential';
1919

2020
import * as validator from './validator';
2121

@@ -69,9 +69,9 @@ export function getProjectId(app: FirebaseApp): string | null {
6969
return options.projectId;
7070
}
7171

72-
const cert: Certificate | null = tryGetCertificate(options.credential);
73-
if (cert != null && validator.isNonEmptyString(cert.projectId)) {
74-
return cert.projectId;
72+
const credential = app.options.credential;
73+
if (credential instanceof ServiceAccountCredential) {
74+
return credential.projectId;
7575
}
7676

7777
const projectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;

test/resources/mocks.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import * as jwt from 'jsonwebtoken';
2727
import {FirebaseNamespace} from '../../src/firebase-namespace';
2828
import {FirebaseServiceInterface} from '../../src/firebase-service';
2929
import {FirebaseApp, FirebaseAppOptions} from '../../src/firebase-app';
30-
import {Certificate, Credential, CertCredential, GoogleOAuthAccessToken} from '../../src/auth/credential';
30+
import {Credential, GoogleOAuthAccessToken, ServiceAccountCredential} from '../../src/auth/credential';
3131

3232
const ALGORITHM = 'RS256';
3333
const ONE_HOUR_IN_SECONDS = 60 * 60;
@@ -49,7 +49,7 @@ export let databaseAuthVariableOverride = { 'some#string': 'some#val' };
4949

5050
export let storageBucket = 'bucketName.appspot.com';
5151

52-
export let credential = new CertCredential(path.resolve(__dirname, './mock.key.json'));
52+
export let credential = new ServiceAccountCredential(path.resolve(__dirname, './mock.key.json'));
5353

5454
export let appOptions: FirebaseAppOptions = {
5555
credential,
@@ -85,10 +85,6 @@ export class MockCredential implements Credential {
8585
expires_in: 3600,
8686
});
8787
}
88-
89-
public getCertificate(): Certificate | null {
90-
return null;
91-
}
9288
}
9389

9490
export function app(): FirebaseApp {
@@ -111,33 +107,33 @@ export function appWithOptions(options: FirebaseAppOptions): FirebaseApp {
111107
}
112108

113109
export function appReturningNullAccessToken(): FirebaseApp {
114-
const nullFn: () => Promise<GoogleOAuthAccessToken>|null = () => null;
110+
const nullFn: () => Promise<GoogleOAuthAccessToken> | null = () => null;
115111
return new FirebaseApp({
116112
credential: {
117113
getAccessToken: nullFn,
118-
getCertificate: () => credential.getCertificate(),
119114
} as any,
120115
databaseURL,
116+
projectId,
121117
}, appName, new FirebaseNamespace().INTERNAL);
122118
}
123119

124120
export function appReturningMalformedAccessToken(): FirebaseApp {
125121
return new FirebaseApp({
126122
credential: {
127123
getAccessToken: () => 5,
128-
getCertificate: () => credential.getCertificate(),
129124
} as any,
130125
databaseURL,
126+
projectId,
131127
}, appName, new FirebaseNamespace().INTERNAL);
132128
}
133129

134130
export function appRejectedWhileFetchingAccessToken(): FirebaseApp {
135131
return new FirebaseApp({
136132
credential: {
137133
getAccessToken: () => Promise.reject(new Error('Promise intentionally rejected.')),
138-
getCertificate: () => credential.getCertificate(),
139134
} as any,
140135
databaseURL,
136+
projectId,
141137
}, appName, new FirebaseNamespace().INTERNAL);
142138
}
143139

test/unit/auth/auth.spec.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
} from '../../../src/auth/auth-config';
4343
import {deepCopy} from '../../../src/utils/deep-copy';
4444
import { TenantManager } from '../../../src/auth/tenant-manager';
45+
import { ServiceAccountCredential } from '../../../src/auth/credential';
4546
import { HttpClient } from '../../../src/utils/api-request';
4647

4748
chai.should();
@@ -365,20 +366,23 @@ AUTH_CONFIGS.forEach((testConfig) => {
365366
});
366367

367368
it('should be fulfilled given an app which returns null access tokens', () => {
369+
getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves(null);
368370
// createCustomToken() does not rely on an access token and therefore works in this scenario.
369-
return nullAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims)
371+
return auth.createCustomToken(mocks.uid, mocks.developerClaims)
370372
.should.eventually.be.fulfilled;
371373
});
372374

373375
it('should be fulfilled given an app which returns invalid access tokens', () => {
376+
getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves('malformed');
374377
// createCustomToken() does not rely on an access token and therefore works in this scenario.
375-
return malformedAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims)
378+
return auth.createCustomToken(mocks.uid, mocks.developerClaims)
376379
.should.eventually.be.fulfilled;
377380
});
378381

379382
it('should be fulfilled given an app which fails to generate access tokens', () => {
383+
getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').rejects('error');
380384
// createCustomToken() does not rely on an access token and therefore works in this scenario.
381-
return rejectedPromiseAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims)
385+
return auth.createCustomToken(mocks.uid, mocks.developerClaims)
382386
.should.eventually.be.fulfilled;
383387
});
384388
});

0 commit comments

Comments
 (0)