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
6 changes: 5 additions & 1 deletion lib/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@
library;

export 'src/logger/logger.dart'
hide createLogger, projectIdZoneKey, traceIdZoneKey;
hide
createLogger,
projectIdZoneKey,
traceIdZoneKey,
cloudTraceContextHeader;
64 changes: 30 additions & 34 deletions lib/src/firebase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import 'eventarc/eventarc_namespace.dart';
import 'firestore/firestore_namespace.dart';
import 'https/https_namespace.dart';
import 'identity/identity_namespace.dart';
import 'logger/logger.dart';
import 'pubsub/pubsub_namespace.dart';
import 'remote_config/remote_config_namespace.dart';
import 'scheduler/scheduler_namespace.dart';
Expand All @@ -39,45 +38,42 @@ import 'test_lab/test_lab_namespace.dart';
///
/// Provides access to all function namespaces (https, pubsub, firestore, etc.).
class Firebase {
Firebase() : _env = FirebaseEnv() {
_initializeAdminSDK();
}

FirebaseApp? _adminApp;
gfs.Firestore? _firestoreInstance;

/// Initialize the Firebase Admin SDK
void _initializeAdminSDK() {
if (_env.isEmulator) {
// TODO: Implement direct REST API calls to emulator
// For now, we'll skip document fetching in emulator mode
return;
}
factory Firebase() {
final env = FirebaseEnv();

// Initialize Admin SDK
final adminApp = FirebaseApp.initializeApp(
options: AppOptions(
credential: Credential.fromApplicationDefaultCredentials(),
projectId: env.projectId,
),
);

// Production mode only
try {
// Initialize Admin SDK
_adminApp = FirebaseApp.initializeApp(
options: AppOptions(
credential: Credential.fromApplicationDefaultCredentials(),
projectId: _env.projectId,
),
);

// Create Firestore instance
_firestoreInstance = _adminApp!.firestore();
} catch (e) {
logger.warn('Failed to initialize Firebase Admin SDK: $e');
}
return Firebase._(
adminApp: adminApp,
firestoreAdmin: adminApp.firestore(),
env: env,
);
}

Firebase._({
required this.adminApp,
required this.firestoreAdmin,
required FirebaseEnv env,
}) : _env = env;

final FirebaseEnv _env;

/// Get the Firestore instance
gfs.Firestore? get firestoreAdmin => _firestoreInstance;
/// The initialized Firebase Admin SDK application instance.
///
/// This app represents the server-side SDK and has elevated privileges
/// corresponding to the environment's credentials.
final FirebaseApp adminApp;

/// Get the Firebase Admin App instance
FirebaseApp? get adminApp => _adminApp;
/// The Firestore admin client instance.
///
/// Provides elevated server-side access to the Firestore database.
final gfs.Firestore firestoreAdmin;

/// HTTPS triggers namespace.
HttpsNamespace get https => HttpsNamespace(this);
Expand Down
36 changes: 9 additions & 27 deletions lib/src/https/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ library;

import 'dart:convert';

import 'package:dart_firebase_admin/app_check.dart';
import 'package:dart_firebase_admin/auth.dart';
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
import 'package:shelf/shelf.dart';

Expand Down Expand Up @@ -59,8 +61,7 @@ final _jwtRegex = RegExp(
/// Returns a tuple of (TokenStatus, AuthData?).
Future<(TokenStatus, AuthData?)> extractAuthToken(
Request request, {
required bool skipTokenVerification,
FirebaseApp? adminApp,
Auth? auth,
}) async {
final authorization = request.headers['authorization'];
if (authorization == null || authorization.isEmpty) {
Expand All @@ -82,7 +83,7 @@ Future<(TokenStatus, AuthData?)> extractAuthToken(
String uid;
Map<String, dynamic>? decodedToken;

if (skipTokenVerification) {
if (auth == null) {
// In emulator mode, just decode without verification
decodedToken = _unsafeDecodeIdToken(idToken);

Expand All @@ -93,12 +94,6 @@ Future<(TokenStatus, AuthData?)> extractAuthToken(
'';
} else {
// In production, verify the token using Firebase Admin SDK
if (adminApp == null) {
// Can't verify without admin app
return (TokenStatus.invalid, null);
}

final auth = adminApp.auth();
final decoded = await auth.verifyIdToken(idToken);
uid = decoded.uid;
decodedToken = {
Expand Down Expand Up @@ -140,8 +135,7 @@ Future<(TokenStatus, AuthData?)> extractAuthToken(
/// Returns a tuple of (TokenStatus, AppCheckData?).
Future<(TokenStatus, AppCheckData?)> extractAppCheckToken(
Request request, {
required bool skipTokenVerification,
FirebaseApp? adminApp,
AppCheck? appCheck,
}) async {
final appCheckToken = request.headers['x-firebase-appcheck'];
if (appCheckToken == null || appCheckToken.isEmpty) {
Expand All @@ -151,7 +145,7 @@ Future<(TokenStatus, AppCheckData?)> extractAppCheckToken(
try {
String appId;

if (skipTokenVerification) {
if (appCheck == null) {
// In emulator mode, just decode without verification
final decodedToken = _unsafeDecodeAppCheckToken(appCheckToken);

Expand All @@ -161,12 +155,6 @@ Future<(TokenStatus, AppCheckData?)> extractAppCheckToken(
'';
} else {
// In production, verify the token using Firebase Admin SDK
if (adminApp == null) {
// Can't verify without admin app
return (TokenStatus.invalid, null);
}

final appCheck = adminApp.appCheck();
final decoded = await appCheck.verifyToken(appCheckToken);
appId = decoded.appId;
}
Expand Down Expand Up @@ -194,21 +182,15 @@ Future<
AppCheckData? appCheckData,
})
>
checkTokens(
Request request, {
required bool skipTokenVerification,
FirebaseApp? adminApp,
}) async {
checkTokens(Request request, {FirebaseApp? adminApp}) async {
final (authStatus, authData) = await extractAuthToken(
request,
skipTokenVerification: skipTokenVerification,
adminApp: adminApp,
auth: adminApp?.auth(),
);

final (appStatus, appCheckData) = await extractAppCheckToken(
request,
skipTokenVerification: skipTokenVerification,
adminApp: adminApp,
appCheck: adminApp?.appCheck(),
);

return (
Expand Down
14 changes: 8 additions & 6 deletions lib/src/https/https_namespace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ class HttpsNamespace extends FunctionsNamespace {
}

// Extract auth and app check tokens
final skipVerification = firebase.$env.skipTokenVerification;

final tokens = await checkTokens(
request,
skipTokenVerification: skipVerification,
adminApp: firebase.adminApp,
adminApp: firebase.$env.skipTokenVerification
? null
: firebase.adminApp,
);

// Check for invalid auth token
Expand Down Expand Up @@ -180,11 +181,12 @@ class HttpsNamespace extends FunctionsNamespace {
final body = await request.json as Map<String, dynamic>?;

// Extract auth and app check tokens
final skipVerification = firebase.$env.skipTokenVerification;

final tokens = await checkTokens(
request,
skipTokenVerification: skipVerification,
adminApp: firebase.adminApp,
adminApp: firebase.$env.skipTokenVerification
? null
: firebase.adminApp,
);

// Check for invalid auth token
Expand Down
66 changes: 15 additions & 51 deletions test/unit/https_auth_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ void main() {
test('returns missing when no Authorization header', () async {
final request = _createRequest();

final (status, auth) = await extractAuthToken(
request,
skipTokenVerification: true,
);
final (status, auth) = await extractAuthToken(request);

expect(status, TokenStatus.missing);
expect(auth, isNull);
Expand All @@ -44,10 +41,7 @@ void main() {
headers: {'authorization': 'Basic abc123'},
);

final (status, auth) = await extractAuthToken(
request,
skipTokenVerification: true,
);
final (status, auth) = await extractAuthToken(request);

expect(status, TokenStatus.invalid);
expect(auth, isNull);
Expand All @@ -58,10 +52,7 @@ void main() {
final jwt = _createJwt({'email': 'test@example.com'});
final request = _createRequest(headers: {'authorization': 'Bearer $jwt'});

final (status, auth) = await extractAuthToken(
request,
skipTokenVerification: true,
);
final (status, auth) = await extractAuthToken(request);

expect(status, TokenStatus.invalid);
expect(auth, isNull);
Expand All @@ -75,10 +66,7 @@ void main() {
});
final request = _createRequest(headers: {'authorization': 'Bearer $jwt'});

final (status, auth) = await extractAuthToken(
request,
skipTokenVerification: true,
);
final (status, auth) = await extractAuthToken(request);

expect(status, TokenStatus.valid);
expect(auth, isNotNull);
Expand All @@ -92,10 +80,7 @@ void main() {
final jwt = _createJwt({'user_id': 'user456'});
final request = _createRequest(headers: {'authorization': 'Bearer $jwt'});

final (status, auth) = await extractAuthToken(
request,
skipTokenVerification: true,
);
final (status, auth) = await extractAuthToken(request);

expect(status, TokenStatus.valid);
expect(auth?.uid, 'user456');
Expand All @@ -105,10 +90,7 @@ void main() {
final jwt = _createJwt({'sub': 'user123'});
final request = _createRequest(headers: {'authorization': 'bearer $jwt'});

final (status, auth) = await extractAuthToken(
request,
skipTokenVerification: true,
);
final (status, auth) = await extractAuthToken(request);

expect(status, TokenStatus.valid);
expect(auth?.uid, 'user123');
Expand All @@ -119,10 +101,7 @@ void main() {
headers: {'authorization': 'Bearer not-a-valid-jwt'},
);

final (status, auth) = await extractAuthToken(
request,
skipTokenVerification: true,
);
final (status, auth) = await extractAuthToken(request);

expect(status, TokenStatus.invalid);
expect(auth, isNull);
Expand All @@ -132,10 +111,7 @@ void main() {
final jwt = _createJwt({});
final request = _createRequest(headers: {'authorization': 'Bearer $jwt'});

final (status, auth) = await extractAuthToken(
request,
skipTokenVerification: true,
);
final (status, auth) = await extractAuthToken(request);

expect(status, TokenStatus.invalid);
expect(auth, isNull);
Expand All @@ -146,10 +122,7 @@ void main() {
test('returns missing when no X-Firebase-AppCheck header', () async {
final request = _createRequest();

final (status, appCheck) = await extractAppCheckToken(
request,
skipTokenVerification: true,
);
final (status, appCheck) = await extractAppCheckToken(request);

expect(status, TokenStatus.missing);
expect(appCheck, isNull);
Expand All @@ -159,10 +132,7 @@ void main() {
final jwt = _createJwt({'other': 'value'});
final request = _createRequest(headers: {'x-firebase-appcheck': jwt});

final (status, appCheck) = await extractAppCheckToken(
request,
skipTokenVerification: true,
);
final (status, appCheck) = await extractAppCheckToken(request);

expect(status, TokenStatus.invalid);
expect(appCheck, isNull);
Expand All @@ -172,10 +142,7 @@ void main() {
final jwt = _createJwt({'sub': 'app123'});
final request = _createRequest(headers: {'x-firebase-appcheck': jwt});

final (status, appCheck) = await extractAppCheckToken(
request,
skipTokenVerification: true,
);
final (status, appCheck) = await extractAppCheckToken(request);

expect(status, TokenStatus.valid);
expect(appCheck, isNotNull);
Expand All @@ -187,10 +154,7 @@ void main() {
final jwt = _createJwt({'sub': 'sub-value', 'app_id': 'explicit-app-id'});
final request = _createRequest(headers: {'x-firebase-appcheck': jwt});

final (status, appCheck) = await extractAppCheckToken(
request,
skipTokenVerification: true,
);
final (status, appCheck) = await extractAppCheckToken(request);

expect(status, TokenStatus.valid);
expect(appCheck?.appId, 'explicit-app-id');
Expand All @@ -208,7 +172,7 @@ void main() {
},
);

final result = await checkTokens(request, skipTokenVerification: true);
final result = await checkTokens(request);

expect(result.result.auth, TokenStatus.valid);
expect(result.result.app, TokenStatus.valid);
Expand All @@ -219,7 +183,7 @@ void main() {
test('returns missing status when headers are absent', () async {
final request = _createRequest();

final result = await checkTokens(request, skipTokenVerification: true);
final result = await checkTokens(request);

expect(result.result.auth, TokenStatus.missing);
expect(result.result.app, TokenStatus.missing);
Expand All @@ -237,7 +201,7 @@ void main() {
},
);

final result = await checkTokens(request, skipTokenVerification: true);
final result = await checkTokens(request);

expect(result.result.auth, TokenStatus.valid);
expect(result.result.app, TokenStatus.invalid);
Expand Down
Loading