Skip to content

Commit 7ce191c

Browse files
authored
feat(core): Add Auth and AppCheck as App's registered service. (#18237)
* Add appCheck registration * Add auth registration * update example for the change * clean up registries in app delete * fix analyzer * fix format * restrict the registry only for firebase service * add deprecation message * make sure we still can have valid instance if auth and appCheck initialized after firebaseai init and before api call * format * address review comment * more test added for firebase ai fetch token
1 parent 7c2fa5b commit 7ce191c

13 files changed

Lines changed: 197 additions & 26 deletions

File tree

packages/firebase_ai/firebase_ai/example/lib/main.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import 'package:firebase_ai/firebase_ai.dart';
16+
1617
import 'package:firebase_auth/firebase_auth.dart';
1718
import 'package:firebase_core/firebase_core.dart';
1819
import 'package:flutter/material.dart';
@@ -70,10 +71,10 @@ class _GenerativeAISampleState extends State<GenerativeAISample> {
7071

7172
void _initializeModel(bool useVertexBackend) {
7273
if (useVertexBackend) {
73-
final vertexInstance = FirebaseAI.vertexAI(auth: FirebaseAuth.instance);
74+
final vertexInstance = FirebaseAI.vertexAI();
7475
_currentModel = vertexInstance.generativeModel(model: 'gemini-2.5-flash');
7576
} else {
76-
final googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance);
77+
final googleAI = FirebaseAI.googleAI();
7778
_currentModel = googleAI.generativeModel(model: 'gemini-2.5-flash');
7879
}
7980
}

packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import 'package:firebase_auth/firebase_auth.dart';
1615
import 'package:flutter/material.dart';
1716
import 'package:firebase_ai/firebase_ai.dart';
1817
import '../widgets/message_widget.dart';
@@ -57,12 +56,12 @@ class _ChatPageState extends State<ChatPage> {
5756
: null,
5857
);
5958
if (widget.useVertexBackend) {
60-
_model = FirebaseAI.vertexAI(auth: FirebaseAuth.instance).generativeModel(
59+
_model = FirebaseAI.vertexAI().generativeModel(
6160
model: 'gemini-2.5-flash',
6261
generationConfig: generationConfig,
6362
);
6463
} else {
65-
_model = FirebaseAI.googleAI(auth: FirebaseAuth.instance).generativeModel(
64+
_model = FirebaseAI.googleAI().generativeModel(
6665
model: 'gemini-2.5-flash',
6766
generationConfig: generationConfig,
6867
);

packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import 'package:flutter/material.dart';
1616
import 'package:firebase_ai/firebase_ai.dart';
17-
import 'package:firebase_auth/firebase_auth.dart';
17+
1818
import '../utils/function_call_utils.dart';
1919
import '../widgets/message_widget.dart';
2020

@@ -235,9 +235,8 @@ class _FunctionCallingPageState extends State<FunctionCallingPage> {
235235
: null,
236236
);
237237

238-
final aiClient = widget.useVertexBackend
239-
? FirebaseAI.vertexAI(auth: FirebaseAuth.instance)
240-
: FirebaseAI.googleAI(auth: FirebaseAuth.instance);
238+
final aiClient =
239+
widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI();
241240

242241
_functionCallModel = aiClient.generativeModel(
243242
model: 'gemini-2.5-flash',

packages/firebase_ai/firebase_ai/example/lib/pages/grounding_page.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import 'package:firebase_auth/firebase_auth.dart';
1615
import 'package:flutter/material.dart';
1716
import 'package:firebase_ai/firebase_ai.dart';
1817
import '../widgets/message_widget.dart';
@@ -74,9 +73,8 @@ class _GroundingPageState extends State<GroundingPage> {
7473
}
7574
}
7675

77-
final aiProvider = widget.useVertexBackend
78-
? FirebaseAI.vertexAI(auth: FirebaseAuth.instance)
79-
: FirebaseAI.googleAI(auth: FirebaseAuth.instance);
76+
final aiProvider =
77+
widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI();
8078

8179
_model = aiProvider.generativeModel(
8280
model: 'gemini-2.5-flash',

packages/firebase_ai/firebase_ai/example/lib/pages/image_generation_page.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import 'dart:typed_data';
1616
import 'package:flutter/material.dart';
1717
import 'package:firebase_ai/firebase_ai.dart';
18-
import 'package:firebase_auth/firebase_auth.dart';
18+
1919
import '../widgets/message_widget.dart';
2020

2121
class ImageGenerationPage extends StatefulWidget {
@@ -47,9 +47,8 @@ class _ImageGenerationPageState extends State<ImageGenerationPage> {
4747
}
4848

4949
void _initializeModel() {
50-
final aiClient = widget.useVertexBackend
51-
? FirebaseAI.vertexAI(auth: FirebaseAuth.instance)
52-
: FirebaseAI.googleAI(auth: FirebaseAuth.instance);
50+
final aiClient =
51+
widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI();
5352

5453
_model = aiClient.generativeModel(
5554
model: 'gemini-2.5-flash-image',

packages/firebase_ai/firebase_ai/lib/src/base_model.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,19 +282,23 @@ abstract class BaseModel {
282282
) {
283283
return () async {
284284
Map<String, String> headers = {};
285+
286+
final effectiveAppCheck = appCheck ?? app?.getService<FirebaseAppCheck>();
287+
final effectiveAuth = auth ?? app?.getService<FirebaseAuth>();
288+
285289
// Override the client name in Google AI SDK
286290
headers['x-goog-api-client'] =
287291
'gl-dart/$packageVersion fire/$packageVersion';
288-
if (appCheck != null) {
292+
if (effectiveAppCheck != null) {
289293
final appCheckToken = useLimitedUseAppCheckTokens == true
290-
? await appCheck.getLimitedUseToken()
291-
: await appCheck.getToken();
294+
? await effectiveAppCheck.getLimitedUseToken()
295+
: await effectiveAppCheck.getToken();
292296
if (appCheckToken != null) {
293297
headers['X-Firebase-AppCheck'] = appCheckToken;
294298
}
295299
}
296-
if (auth != null) {
297-
final idToken = await auth.currentUser?.getIdToken();
300+
if (effectiveAuth != null) {
301+
final idToken = await effectiveAuth.currentUser?.getIdToken();
298302
if (idToken != null) {
299303
headers['Authorization'] = 'Firebase $idToken';
300304
}

packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,18 @@ class FirebaseAI extends FirebasePluginPlatform {
6262
/// If pass in [appCheck], request session will get protected from abusing.
6363
static FirebaseAI vertexAI({
6464
FirebaseApp? app,
65+
@Deprecated(
66+
'Passing an explicit instance is deprecated, internal handling is now automatic.')
6567
FirebaseAppCheck? appCheck,
68+
@Deprecated(
69+
'Passing an explicit instance is deprecated, internal handling is now automatic.')
6670
FirebaseAuth? auth,
6771
String? location,
6872
bool? useLimitedUseAppCheckTokens,
6973
}) {
7074
app ??= Firebase.app();
75+
appCheck ??= app.getService<FirebaseAppCheck>();
76+
auth ??= app.getService<FirebaseAuth>();
7177
var instanceKey = '${app.name}::vertexai::$location';
7278

7379
if (_cachedInstances.containsKey(instanceKey)) {
@@ -95,11 +101,17 @@ class FirebaseAI extends FirebasePluginPlatform {
95101
/// If pass in [appCheck], request session will get protected from abusing.
96102
static FirebaseAI googleAI({
97103
FirebaseApp? app,
104+
@Deprecated(
105+
'Passing an explicit instance is deprecated, internal handling is now automatic.')
98106
FirebaseAppCheck? appCheck,
107+
@Deprecated(
108+
'Passing an explicit instance is deprecated, internal handling is now automatic.')
99109
FirebaseAuth? auth,
100110
bool? useLimitedUseAppCheckTokens,
101111
}) {
102112
app ??= Firebase.app();
113+
appCheck ??= app.getService<FirebaseAppCheck>();
114+
auth ??= app.getService<FirebaseAuth>();
103115
var instanceKey = '${app.name}::googleai';
104116

105117
if (_cachedInstances.containsKey(instanceKey)) {

packages/firebase_ai/firebase_ai/test/base_model_test.dart

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ class MockFirebaseApp extends Mock implements FirebaseApp {
3131

3232
@override
3333
bool get isAutomaticDataCollectionEnabled => true;
34+
35+
FirebaseAppCheck? mockAppCheck;
36+
FirebaseAuth? mockAuth;
37+
38+
@override
39+
T? getService<T extends FirebaseService>() {
40+
if (T == FirebaseAppCheck) {
41+
return mockAppCheck as T?;
42+
}
43+
if (T == FirebaseAuth) {
44+
return mockAuth as T?;
45+
}
46+
return null;
47+
}
3448
}
3549

3650
// Mock FirebaseOptions
@@ -131,6 +145,84 @@ void main() {
131145
expect(headers.length, 2);
132146
});
133147

148+
test('firebaseTokens discovers App Check token dynamically at request time',
149+
() async {
150+
final mockApp = MockFirebaseApp();
151+
final mockAppCheck = MockFirebaseAppCheck();
152+
when(mockAppCheck.getToken())
153+
.thenAnswer((_) async => 'dynamic-app-check-token');
154+
mockApp.mockAppCheck = mockAppCheck;
155+
156+
final tokenFunction =
157+
BaseModel.firebaseTokens(null, null, mockApp, false);
158+
final headers = await tokenFunction();
159+
160+
expect(headers['X-Firebase-AppCheck'], 'dynamic-app-check-token');
161+
expect(headers['X-Firebase-AppId'], 'test-app-id');
162+
expect(headers.length, 3);
163+
});
164+
165+
test('firebaseTokens discovers Auth ID token dynamically at request time',
166+
() async {
167+
final mockApp = MockFirebaseApp();
168+
final mockAuth = MockFirebaseAuth();
169+
final mockUser = MockUser();
170+
when(mockUser.getIdToken()).thenAnswer((_) async => 'dynamic-id-token');
171+
when(mockAuth.currentUser).thenReturn(mockUser);
172+
mockApp.mockAuth = mockAuth;
173+
174+
final tokenFunction =
175+
BaseModel.firebaseTokens(null, null, mockApp, false);
176+
final headers = await tokenFunction();
177+
178+
expect(headers['Authorization'], 'Firebase dynamic-id-token');
179+
expect(headers['X-Firebase-AppId'], 'test-app-id');
180+
expect(headers.length, 3);
181+
});
182+
183+
test('firebaseTokens discovers both tokens dynamically at request time',
184+
() async {
185+
final mockApp = MockFirebaseApp();
186+
final mockAppCheck = MockFirebaseAppCheck();
187+
final mockAuth = MockFirebaseAuth();
188+
final mockUser = MockUser();
189+
190+
when(mockAppCheck.getToken())
191+
.thenAnswer((_) async => 'dynamic-app-check-token');
192+
when(mockUser.getIdToken()).thenAnswer((_) async => 'dynamic-id-token');
193+
when(mockAuth.currentUser).thenReturn(mockUser);
194+
195+
mockApp.mockAppCheck = mockAppCheck;
196+
mockApp.mockAuth = mockAuth;
197+
198+
final tokenFunction =
199+
BaseModel.firebaseTokens(null, null, mockApp, false);
200+
final headers = await tokenFunction();
201+
202+
expect(headers['X-Firebase-AppCheck'], 'dynamic-app-check-token');
203+
expect(headers['Authorization'], 'Firebase dynamic-id-token');
204+
expect(headers['X-Firebase-AppId'], 'test-app-id');
205+
expect(headers.length, 4);
206+
});
207+
208+
test(
209+
'firebaseTokens discovers App Check token dynamically with limited use',
210+
() async {
211+
final mockApp = MockFirebaseApp();
212+
final mockAppCheck = MockFirebaseAppCheck();
213+
214+
when(mockAppCheck.getLimitedUseToken())
215+
.thenAnswer((_) async => 'dynamic-limited-use-token');
216+
mockApp.mockAppCheck = mockAppCheck;
217+
218+
final tokenFunction = BaseModel.firebaseTokens(null, null, mockApp, true);
219+
final headers = await tokenFunction();
220+
221+
expect(headers['X-Firebase-AppCheck'], 'dynamic-limited-use-token');
222+
expect(headers['X-Firebase-AppId'], 'test-app-id');
223+
expect(headers.length, 3);
224+
});
225+
134226
test('firebaseTokens includes all tokens if available', () async {
135227
final mockAppCheck = MockFirebaseAppCheck();
136228
when(mockAppCheck.getToken())

packages/firebase_ai/firebase_ai/test/firebase_vertexai_test.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import 'package:firebase_ai/firebase_ai.dart';
1616
import 'package:firebase_app_check/firebase_app_check.dart';
17+
import 'package:firebase_auth/firebase_auth.dart';
1718
import 'package:firebase_core/firebase_core.dart';
1819
import 'package:flutter_test/flutter_test.dart';
1920

@@ -28,6 +29,7 @@ void main() {
2829
late FirebaseApp customApp;
2930
late FirebaseApp limitTokenApp;
3031
late FirebaseAppCheck customAppCheck;
32+
late FirebaseAuth customAuth;
3133
late FirebaseAppCheck limitTokenAppCheck;
3234

3335
group('FirebaseAI Tests', () {
@@ -47,6 +49,7 @@ void main() {
4749
appCheck = FirebaseAppCheck.instance;
4850
customAppCheck = FirebaseAppCheck.instanceFor(app: customApp);
4951
limitTokenAppCheck = FirebaseAppCheck.instanceFor(app: limitTokenApp);
52+
customAuth = FirebaseAuth.instanceFor(app: customApp);
5053
});
5154

5255
test('Singleton behavior', () {
@@ -96,6 +99,20 @@ void main() {
9699
expect(vertexAIAppCheck.useLimitedUseAppCheckTokens, true);
97100
});
98101

102+
test('Instance creation with auto-injected AppCheck', () {
103+
final vertexAI = FirebaseAI.vertexAI(app: customApp);
104+
105+
expect(vertexAI.app, equals(customApp));
106+
expect(vertexAI.appCheck, equals(customAppCheck));
107+
});
108+
109+
test('Instance creation with auto-injected Auth', () {
110+
final vertexAI = FirebaseAI.vertexAI(app: customApp);
111+
112+
expect(vertexAI.app, equals(customApp));
113+
expect(vertexAI.auth, equals(customAuth));
114+
});
115+
99116
test('generativeModel creation with Grounding tools', () {
100117
final ai = FirebaseAI.googleAI();
101118

packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
part of '../firebase_app_check.dart';
77

8-
class FirebaseAppCheck extends FirebasePluginPlatform {
8+
class FirebaseAppCheck extends FirebasePluginPlatform
9+
implements FirebaseService {
910
static Map<String, FirebaseAppCheck> _firebaseAppCheckInstances = {};
1011

1112
FirebaseAppCheck._({required this.app})
@@ -41,7 +42,9 @@ class FirebaseAppCheck extends FirebasePluginPlatform {
4142
/// Returns an instance using a specified [FirebaseApp].
4243
static FirebaseAppCheck instanceFor({required FirebaseApp app}) {
4344
return _firebaseAppCheckInstances.putIfAbsent(app.name, () {
44-
return FirebaseAppCheck._(app: app);
45+
final instance = FirebaseAppCheck._(app: app);
46+
app.registerService<FirebaseAppCheck>(instance);
47+
return instance;
4548
});
4649
}
4750

0 commit comments

Comments
 (0)