Skip to content

Commit 60e8347

Browse files
authored
Make App Check initialization explicit (#4897)
1 parent 8bece48 commit 60e8347

File tree

8 files changed

+215
-19
lines changed

8 files changed

+215
-19
lines changed

.changeset/fair-garlics-smoke.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/app': patch
3+
'@firebase/app-check': patch
4+
---
5+
6+
Make App Check initialization explicit, to prevent unexpected errors for users who do not intend to use App Check.

packages-exp/app-compat/src/firebaseApp.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
Component,
2121
ComponentContainer,
2222
ComponentType,
23+
InstantiationMode,
2324
Name
2425
} from '@firebase/component';
2526
import {
@@ -121,8 +122,17 @@ export class FirebaseAppImpl implements Compat<_FirebaseAppExp>, _FirebaseApp {
121122
): _FirebaseService {
122123
this._delegate.checkDestroyed();
123124

125+
// Initialize instance if InstatiationMode is `EXPLICIT`.
126+
const provider = this._delegate.container.getProvider(name as Name);
127+
if (
128+
!provider.isInitialized() &&
129+
provider.getComponent()?.instantiationMode === InstantiationMode.EXPLICIT
130+
) {
131+
provider.initialize();
132+
}
133+
124134
// getImmediate will always succeed because _getService is only called for registered components.
125-
return (this._delegate.container.getProvider(name as Name).getImmediate({
135+
return (provider.getImmediate({
126136
identifier: instanceIdentifier
127137
}) as unknown) as _FirebaseService;
128138
}

packages-exp/app-compat/test/firebaseAppCompat.test.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import { stub } from 'sinon';
2121
import { FirebaseNamespace, FirebaseOptions } from '../src/public-types';
2222
import { _FirebaseApp, _FirebaseNamespace } from '../src/types';
2323
import { _components, _clearComponents } from '@firebase/app-exp';
24-
import { ComponentType } from '@firebase/component';
24+
import {
25+
Component,
26+
ComponentType,
27+
InstantiationMode
28+
} from '@firebase/component';
2529

2630
import { createFirebaseNamespace } from '../src/firebaseNamespace';
2731
import { createFirebaseNamespaceLite } from '../src/lite/firebaseNamespaceLite';
@@ -67,6 +71,7 @@ function executeFirebaseTests(): void {
6771

6872
expect(serviceNamespace).to.eq(serviceNamespace2);
6973
expect(registerStub).to.have.not.thrown();
74+
registerStub.restore();
7075
});
7176

7277
it('returns cached service instances', () => {
@@ -80,6 +85,71 @@ function executeFirebaseTests(): void {
8085
expect(service).to.eq((firebase as any).test());
8186
});
8287

88+
it('does not instantiate explicit components unless called explicitly', () => {
89+
firebase.initializeApp({});
90+
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
91+
createTestComponent('explicit1').setInstantiationMode(
92+
InstantiationMode.EXPLICIT
93+
)
94+
);
95+
96+
let explicitService;
97+
98+
// Expect getImmediate in a consuming component to return null.
99+
const consumerComponent = new Component(
100+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
101+
'consumer' as any,
102+
container => {
103+
explicitService = container
104+
.getProvider('explicit1' as any)
105+
.getImmediate({ optional: true });
106+
return new TestService(
107+
container.getProvider('app-compat').getImmediate()
108+
);
109+
},
110+
ComponentType.PUBLIC
111+
);
112+
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
113+
consumerComponent
114+
);
115+
116+
(firebase as any).consumer();
117+
expect(explicitService).to.be.null;
118+
});
119+
120+
it('does instantiate explicit components when called explicitly', () => {
121+
firebase.initializeApp({});
122+
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
123+
createTestComponent('explicit2').setInstantiationMode(
124+
InstantiationMode.EXPLICIT
125+
)
126+
);
127+
128+
let explicitService;
129+
130+
// Expect getImmediate in a consuming component to return the service.
131+
const consumerComponent = new Component(
132+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
133+
'consumer' as any,
134+
container => {
135+
explicitService = container
136+
.getProvider('explicit2' as any)
137+
.getImmediate({ optional: true });
138+
return new TestService(
139+
container.getProvider('app-compat').getImmediate()
140+
);
141+
},
142+
ComponentType.PUBLIC
143+
);
144+
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
145+
consumerComponent
146+
);
147+
148+
(firebase as any).explicit2();
149+
(firebase as any).consumer();
150+
expect(explicitService).to.not.be.null;
151+
});
152+
83153
it(`creates a new instance of a service after removing the existing instance`, () => {
84154
const app = firebase.initializeApp({});
85155
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(

packages/app-check/src/index.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
*/
1717
import firebase from '@firebase/app';
1818
import { _FirebaseNamespace } from '@firebase/app-types/private';
19-
import { Component, ComponentType } from '@firebase/component';
19+
import {
20+
Component,
21+
ComponentType,
22+
InstantiationMode
23+
} from '@firebase/component';
2024
import {
2125
FirebaseAppCheck,
2226
AppCheckComponentName
@@ -41,6 +45,26 @@ function registerAppCheck(firebase: _FirebaseNamespace): void {
4145
},
4246
ComponentType.PUBLIC
4347
)
48+
/**
49+
* AppCheck can only be initialized by explicitly calling firebase.appCheck()
50+
* We don't want firebase products that consume AppCheck to gate on AppCheck
51+
* if the user doesn't intend them to, just because the AppCheck component
52+
* is registered.
53+
*/
54+
.setInstantiationMode(InstantiationMode.EXPLICIT)
55+
/**
56+
* Because all firebase products that depend on app-check depend on app-check-internal directly,
57+
* we need to initialize app-check-internal after app-check is initialized to make it
58+
* available to other firebase products.
59+
*/
60+
.setInstanceCreatedCallback(
61+
(container, _instanceIdentifier, _instance) => {
62+
const appCheckInternalProvider = container.getProvider(
63+
APP_CHECK_NAME_INTERNAL
64+
);
65+
appCheckInternalProvider.initialize();
66+
}
67+
)
4468
);
4569

4670
// The internal interface used by other Firebase products
@@ -54,7 +78,7 @@ function registerAppCheck(firebase: _FirebaseNamespace): void {
5478
return internalFactory(app, platformLoggerProvider);
5579
},
5680
ComponentType.PUBLIC
57-
)
81+
).setInstantiationMode(InstantiationMode.EXPLICIT)
5882
);
5983

6084
firebase.registerVersion(name, version);

packages/app/src/firebaseApp.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
ComponentContainer,
3030
Component,
3131
ComponentType,
32-
Name
32+
Name,
33+
InstantiationMode
3334
} from '@firebase/component';
3435
import { AppError, ERROR_FACTORY } from './errors';
3536
import { DEFAULT_ENTRY_NAME } from './constants';
@@ -122,8 +123,17 @@ export class FirebaseAppImpl implements FirebaseApp {
122123
): FirebaseService {
123124
this.checkDestroyed_();
124125

126+
// Initialize instance if InstatiationMode is `EXPLICIT`.
127+
const provider = this.container.getProvider(name as Name);
128+
if (
129+
!provider.isInitialized() &&
130+
provider.getComponent()?.instantiationMode === InstantiationMode.EXPLICIT
131+
) {
132+
provider.initialize();
133+
}
134+
125135
// getImmediate will always succeed because _getService is only called for registered components.
126-
return (this.container.getProvider(name as Name).getImmediate({
136+
return (provider.getImmediate({
127137
identifier: instanceIdentifier
128138
}) as unknown) as FirebaseService;
129139
}

packages/app/test/clientLogger.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
*/
1717

1818
import { FirebaseNamespace, VersionService } from '@firebase/app-types';
19-
import { _FirebaseNamespace } from '@firebase/app-types/private';
19+
import {
20+
_FirebaseNamespace,
21+
FirebaseService
22+
} from '@firebase/app-types/private';
2023
import { createFirebaseNamespace } from '../src/firebaseNamespace';
2124
import { expect } from 'chai';
2225
import { spy as Spy } from 'sinon';
@@ -29,7 +32,7 @@ declare module '@firebase/component' {
2932
interface NameServiceMapping {
3033
'vs1': VersionService;
3134
'vs2': VersionService;
32-
'test-shell': Promise<void>;
35+
'test-shell': FirebaseService;
3336
}
3437
}
3538

@@ -51,7 +54,7 @@ describe('User Log Methods', () => {
5154
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
5255
new Component(
5356
'test-shell',
54-
async () => {
57+
() => {
5558
const logger = new Logger('@firebase/logger-test');
5659
logger.warn('hello');
5760
expect(warnSpy.called).to.be.true;
@@ -67,6 +70,7 @@ describe('User Log Methods', () => {
6770
expect(infoSpy.called).to.be.true;
6871
logger.log('hi');
6972
expect(logSpy.called).to.be.true;
73+
return {} as FirebaseService;
7074
},
7175
ComponentType.PUBLIC
7276
)
@@ -81,7 +85,7 @@ describe('User Log Methods', () => {
8185
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
8286
new Component(
8387
'test-shell',
84-
async () => {
88+
() => {
8589
const logger = new Logger('@firebase/logger-test');
8690
(firebase as _FirebaseNamespace).onLog(logData => {
8791
result = logData;
@@ -92,6 +96,7 @@ describe('User Log Methods', () => {
9296
expect(result.args).to.deep.equal(['hi']);
9397
expect(result.type).to.equal('@firebase/logger-test');
9498
expect(infoSpy.called).to.be.true;
99+
return {} as FirebaseService;
95100
},
96101
ComponentType.PUBLIC
97102
)

packages/app/test/firebaseApp.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import { createFirebaseNamespace } from '../src/firebaseNamespace';
2929
import { createFirebaseNamespaceLite } from '../src/lite/firebaseNamespaceLite';
3030
import { expect } from 'chai';
3131
import { stub } from 'sinon';
32-
import { Component, ComponentType } from '@firebase/component';
32+
import {
33+
Component,
34+
ComponentType,
35+
InstantiationMode
36+
} from '@firebase/component';
3337
import './setup';
3438

3539
executeFirebaseTests();
@@ -80,6 +84,67 @@ function executeFirebaseTests(): void {
8084
expect(service).to.eq((firebase as any).test());
8185
});
8286

87+
it('does not instantiate explicit components unless called explicitly', () => {
88+
firebase.initializeApp({});
89+
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
90+
createTestComponent('test').setInstantiationMode(
91+
InstantiationMode.EXPLICIT
92+
)
93+
);
94+
95+
let explicitService;
96+
97+
// Expect getImmediate in a consuming component to return null.
98+
const consumerComponent = new Component(
99+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
100+
'consumer' as any,
101+
container => {
102+
explicitService = container
103+
.getProvider('test' as any)
104+
.getImmediate({ optional: true });
105+
return new TestService(container.getProvider('app').getImmediate());
106+
},
107+
ComponentType.PUBLIC
108+
);
109+
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
110+
consumerComponent
111+
);
112+
113+
(firebase as any).consumer();
114+
expect(explicitService).to.be.null;
115+
});
116+
117+
it('does instantiate explicit components when called explicitly', () => {
118+
firebase.initializeApp({});
119+
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
120+
createTestComponent('test').setInstantiationMode(
121+
InstantiationMode.EXPLICIT
122+
)
123+
);
124+
125+
let explicitService;
126+
127+
// Expect getImmediate in a consuming component to return the service.
128+
const consumerComponent = new Component(
129+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
130+
'consumer' as any,
131+
container => {
132+
explicitService = container
133+
.getProvider('test' as any)
134+
.getImmediate({ optional: true });
135+
return new TestService(container.getProvider('app').getImmediate());
136+
},
137+
ComponentType.PUBLIC
138+
);
139+
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
140+
consumerComponent
141+
);
142+
143+
(firebase as any).test();
144+
(firebase as any).consumer();
145+
expect(explicitService).to.not.be.null;
146+
});
147+
83148
it(`creates a new instance of a service after removing the existing instance`, () => {
84149
const app = firebase.initializeApp({});
85150
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(

0 commit comments

Comments
 (0)