Skip to content
Draft
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
7 changes: 1 addition & 6 deletions packages/dart_firebase_admin/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import 'package:dart_firebase_admin/firestore.dart';
import 'package:dart_firebase_admin/messaging.dart';

Future<void> main() async {
final admin = FirebaseAdminApp.initializeApp(
'dart-firebase-admin',
Credential.fromApplicationDefaultCredentials(),
);

// // admin.useEmulator();
final admin = FirebaseAdmin.initializeApp();

final messaging = Messaging(admin);

Expand Down
2 changes: 1 addition & 1 deletion packages/dart_firebase_admin/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import 'dart:io';

import 'package:googleapis/identitytoolkit/v3.dart' as auth3;
import 'package:googleapis_auth/auth_io.dart' as auth;
import 'package:googleapis_auth/googleapis_auth.dart';
import 'package:http/http.dart';
import 'package:meta/meta.dart';

part 'firebase_admin.dart';
part 'app/credential.dart';
part 'app/exception.dart';
part 'app/firebase_admin.dart';
224 changes: 184 additions & 40 deletions packages/dart_firebase_admin/lib/src/app/credential.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,51 +43,107 @@ class _EmulatorClient extends BaseClient {
}
}

/// Authentication information for Firebase Admin SDK.
class Credential {
Credential._(
this.serviceAccountCredentials, {
this.serviceAccountId,
}) : assert(
serviceAccountId == null || serviceAccountCredentials == null,
'Cannot specify both serviceAccountId and serviceAccountCredentials',
);

/// Log in to firebase from a service account file.
/// Base class for Firebase Admin SDK credentials.
///
/// Use [ServiceAccountCredential] for service account credentials,
/// or [ApplicationDefaultCredential] for Application Default Credentials.
sealed class Credential {
/// Factory to create a credential using Application Default Credentials.
factory Credential.fromApplicationDefaultCredentials({
String? serviceAccountId,
}) {
return ApplicationDefaultCredential.fromEnvironment(
serviceAccountId: serviceAccountId,
);
}

/// Factory to create a credential from a service account file.
factory Credential.fromServiceAccount(File serviceAccountFile) {
final content = serviceAccountFile.readAsStringSync();
return ServiceAccountCredential.fromFile(serviceAccountFile);
}

/// Private constructor for sealed class.
Credential._();

/// Returns the underlying [auth.ServiceAccountCredentials] if this is a
/// [ServiceAccountCredential], null otherwise.
@internal
auth.ServiceAccountCredentials? get serviceAccountCredentials;

/// Returns the service account ID (email) if available.
@internal
String? get serviceAccountId;
}

/// Extended service account credentials that includes projectId.
///
/// This wraps [auth.ServiceAccountCredentials] and adds the [projectId] field
/// which is required for Firebase Admin SDK operations.
@internal
final class ServiceAccountCredential extends Credential {
/// Creates a [ServiceAccountCredential] from a JSON object.
factory ServiceAccountCredential.fromJson(Map<String, Object?> json) {
// Extract and validate projectId - required for service accounts
final projectId = json['project_id'] as String?;
if (projectId == null || projectId.isEmpty) {
throw const FormatException(
'Service account JSON must contain a "project_id" property',
);
}

// Use parent's fromJson to create the base credentials
final credentials = auth.ServiceAccountCredentials.fromJson(json);

return ServiceAccountCredential._(credentials, projectId);
}

/// Creates a [ServiceAccountCredential] from a service account JSON file.
factory ServiceAccountCredential.fromFile(File serviceAccountFile) {
final content = serviceAccountFile.readAsStringSync();
final json = jsonDecode(content);
if (json is! Map<String, Object?>) {
throw const FormatException('Invalid service account file');
}

final serviceAccountCredentials =
auth.ServiceAccountCredentials.fromJson(json);

return Credential._(serviceAccountCredentials);
return ServiceAccountCredential.fromJson(json);
}
ServiceAccountCredential._(
this._credentials,
this.projectId,
) : super._();

/// Log in to firebase from a service account file parameters.
factory Credential.fromServiceAccountParams({
required String clientId,
required String privateKey,
required String email,
}) {
final serviceAccountCredentials = auth.ServiceAccountCredentials(
email,
ClientId(clientId),
privateKey,
);
final auth.ServiceAccountCredentials _credentials;

return Credential._(serviceAccountCredentials);
}
/// The Google Cloud project ID associated with this service account.
final String projectId;

/// Log in to firebase using the environment variable.
factory Credential.fromApplicationDefaultCredentials({
/// The service account email (client_email).
String get clientEmail => _credentials.email;

/// The private key.
String get privateKey => _credentials.privateKey;

@override
auth.ServiceAccountCredentials get serviceAccountCredentials => _credentials;

@override
String? get serviceAccountId => _credentials.email;
}

/// Application Default Credentials for Firebase Admin SDK.
///
/// Uses Google Application Default Credentials (ADC) which can be:
/// - A service account file specified via GOOGLE_APPLICATION_CREDENTIALS
/// - Compute Engine default service account
/// - Other ADC sources
@internal
final class ApplicationDefaultCredential extends Credential {
/// Factory to create from environment (GOOGLE_APPLICATION_CREDENTIALS).
factory ApplicationDefaultCredential.fromEnvironment({
String? serviceAccountId,
}) {
ServiceAccountCredentials? creds;
auth.ServiceAccountCredentials? creds;
String? projectId;

final env =
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
Expand All @@ -97,20 +153,108 @@ class Credential {
final text = File(maybeConfig).readAsStringSync();
final decodedValue = jsonDecode(text);
if (decodedValue is Map) {
creds = ServiceAccountCredentials.fromJson(decodedValue);
creds = auth.ServiceAccountCredentials.fromJson(decodedValue);
projectId = decodedValue['project_id'] as String?;
}
} on FormatException catch (_) {}
} on FormatException catch (_) {
// Ignore parsing errors, will fall back to metadata service
}
}

return Credential._(
creds,
return ApplicationDefaultCredential(
serviceAccountId: serviceAccountId,
serviceAccountCredentials: creds,
projectId: projectId,
);
}
ApplicationDefaultCredential({
String? serviceAccountId,
auth.ServiceAccountCredentials? serviceAccountCredentials,
String? projectId,
}) : _serviceAccountId = serviceAccountId,
_serviceAccountCredentials = serviceAccountCredentials,
_projectId = projectId,
super._();

@internal
final String? serviceAccountId;
final String? _serviceAccountId;
final auth.ServiceAccountCredentials? _serviceAccountCredentials;
final String? _projectId;

@internal
final auth.ServiceAccountCredentials? serviceAccountCredentials;
@override
auth.ServiceAccountCredentials? get serviceAccountCredentials =>
_serviceAccountCredentials;

@override
String? get serviceAccountId =>
_serviceAccountId ?? _serviceAccountCredentials?.email;

/// The project ID if available from the service account file.
/// For Compute Engine, this needs to be fetched asynchronously via metadata service.
String? get projectId => _projectId;

/// Fetches the project ID from the metadata service (for Compute Engine).
/// Returns null if not available.
Future<String?> getProjectId() async {
if (_projectId != null) {
return _projectId;
}

// Try to get from metadata service
try {
final response = await get(
Uri.parse(
'http://metadata/computeMetadata/v1/project/project-id',
),
headers: {
'Metadata-Flavor': 'Google',
},
);

if (response.statusCode == 200) {
return response.body;
}
} catch (_) {
// Not on Compute Engine or metadata service unavailable
}

return null;
}

/// Fetches the service account email from the metadata service (for Compute Engine).
/// Returns null if not available.
Future<String?> getServiceAccountEmail() async {
if (_serviceAccountId != null) {
return _serviceAccountId;
}

if (_serviceAccountCredentials != null) {
return _serviceAccountCredentials.email;
}

// Try to get from metadata service
try {
final response = await get(
Uri.parse(
'http://metadata/computeMetadata/v1/instance/service-accounts/default/email',
),
headers: {
'Metadata-Flavor': 'Google',
},
);

if (response.statusCode == 200) {
return response.body;
}
} catch (_) {
// Not on Compute Engine or metadata service unavailable
}

return null;
}
}

/// Helper function to get Application Default Credential.
@internal
ApplicationDefaultCredential getApplicationDefault() {
return ApplicationDefaultCredential.fromEnvironment();
}
21 changes: 21 additions & 0 deletions packages/dart_firebase_admin/lib/src/app/exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,24 @@ abstract class FirebaseAdminException implements Exception {
return '$runtimeType($code, $message)';
}
}

/// App client error codes.
class AppErrorCode {
static const appDeleted = 'app-deleted';
static const duplicateApp = 'duplicate-app';
static const invalidArgument = 'invalid-argument';
static const internalError = 'internal-error';
static const invalidAppName = 'invalid-app-name';
static const invalidAppOptions = 'invalid-app-options';
static const invalidCredential = 'invalid-credential';
static const networkError = 'network-error';
static const networkTimeout = 'network-timeout';
static const noApp = 'no-app';
static const unableToParseResponse = 'unable-to-parse-response';
}

/// Exception thrown for Firebase App-related errors.
class FirebaseAppException extends FirebaseAdminException {
FirebaseAppException(String code, [String? message])
: super('app', code, message);
}
Loading
Loading