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
1 change: 1 addition & 0 deletions packages/dart_firebase_admin/lib/dart_firebase_admin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export 'src/app.dart'
FirebaseService,
FirebaseServiceType,
FirebaseUserAgentClient,
RefreshTokenCredential,
ServiceAccountCredential,
envSymbol;
151 changes: 151 additions & 0 deletions packages/dart_firebase_admin/lib/src/app/credential.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const envSymbol = #_envSymbol;
/// - [Credential.fromServiceAccount] - For service account JSON files
/// - [Credential.fromServiceAccountParams] - For individual service account parameters
/// - [Credential.fromApplicationDefaultCredentials] - For Application Default Credentials (ADC)
/// - [Credential.fromRefreshToken] - For OAuth2 refresh token JSON files
/// - [Credential.fromRefreshTokenParams] - For individual OAuth2 refresh token parameters
///
/// The credential is used to authenticate all API calls made by the Admin SDK.
sealed class Credential {
Expand Down Expand Up @@ -115,6 +117,126 @@ sealed class Credential {
}
}

/// Creates a credential from an OAuth2 refresh token JSON file.
///
/// The file must contain:
/// - `client_id`: The OAuth2 client ID
/// - `client_secret`: The OAuth2 client secret
/// - `refresh_token`: The refresh token
/// - `type`: The credential type (typically `"authorized_user"`)
///
/// You can obtain a refresh token JSON file by running:
/// ```
/// gcloud auth application-default login
/// ```
/// which writes credentials to `~/.config/gcloud/application_default_credentials.json`.
///
/// Example:
/// ```dart
/// final credential = Credential.fromRefreshToken(
/// File('path/to/refresh_token.json'),
/// );
/// ```
factory Credential.fromRefreshToken(File refreshTokenFile) {
final String raw;
try {
raw = refreshTokenFile.readAsStringSync();
} on IOException catch (e) {
throw FirebaseAppException(
AppErrorCode.invalidCredential,
'Failed to read refresh token file: $e',
);
}

final Object? json;
try {
json = jsonDecode(raw);
} on FormatException catch (e) {
throw FirebaseAppException(
AppErrorCode.invalidCredential,
'Failed to parse refresh token JSON: ${e.message}',
);
}

if (json
case {
'client_id': final String clientId,
'client_secret': final String clientSecret,
'refresh_token': final String refreshToken,
'type': final String type,
}
when clientId.isNotEmpty &&
clientSecret.isNotEmpty &&
refreshToken.isNotEmpty &&
type.isNotEmpty) {
return RefreshTokenCredential._(
clientId: clientId,
clientSecret: clientSecret,
refreshToken: refreshToken,
);
}

throw FirebaseAppException(
AppErrorCode.invalidCredential,
'Refresh token file must contain non-empty string fields: '
'"client_id", "client_secret", "refresh_token", and "type".',
);
}

/// Creates a credential from individual OAuth2 refresh token parameters.
///
/// Parameters:
/// - [clientId]: The OAuth2 client ID
/// - [clientSecret]: The OAuth2 client secret
/// - [refreshToken]: The refresh token
/// - [type]: The credential type (typically `"authorized_user"`)
///
/// Example:
/// ```dart
/// final credential = Credential.fromRefreshTokenParams(
/// clientId: 'my-client-id',
/// clientSecret: 'my-client-secret',
/// refreshToken: 'my-refresh-token',
/// type: 'authorized_user',
/// );
/// ```
factory Credential.fromRefreshTokenParams({
required String clientId,
required String clientSecret,
required String refreshToken,
String type = 'authorized_user',
}) {
if (clientId.isEmpty) {
throw FirebaseAppException(
AppErrorCode.invalidCredential,
'Refresh token must contain a non-empty "client_id".',
);
}
if (clientSecret.isEmpty) {
throw FirebaseAppException(
AppErrorCode.invalidCredential,
'Refresh token must contain a non-empty "client_secret".',
);
}
if (refreshToken.isEmpty) {
throw FirebaseAppException(
AppErrorCode.invalidCredential,
'Refresh token must contain a non-empty "refresh_token".',
);
}
if (type.isEmpty) {
throw FirebaseAppException(
AppErrorCode.invalidCredential,
'Refresh token must contain a non-empty "type".',
);
}
return RefreshTokenCredential._(
clientId: clientId,
clientSecret: clientSecret,
refreshToken: refreshToken,
);
}

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

Expand Down Expand Up @@ -170,6 +292,35 @@ final class ServiceAccountCredential extends Credential {
String? get serviceAccountId => _serviceAccountCredentials.email;
}

/// OAuth2 refresh token credentials for Firebase Admin SDK.
///
/// Uses a refresh token to obtain and automatically refresh access tokens.
/// Obtain a refresh token file by running `gcloud auth application-default login`.
@internal
final class RefreshTokenCredential extends Credential {
RefreshTokenCredential._({
required this.clientId,
required this.clientSecret,
required this.refreshToken,
}) : super._();

/// The OAuth2 client ID.
final String clientId;

/// The OAuth2 client secret.
final String clientSecret;

/// The OAuth2 refresh token.
final String refreshToken;

@override
googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials =>
null;

@override
String? get serviceAccountId => null;
}

/// Application Default Credentials for Firebase Admin SDK.
///
/// Uses Google Application Default Credentials (ADC) to automatically discover
Expand Down
10 changes: 10 additions & 0 deletions packages/dart_firebase_admin/lib/src/app/firebase_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ class FirebaseApp {
serviceAccountCredentials,
scopes,
),
RefreshTokenCredential(
:final clientId,
:final clientSecret,
:final refreshToken,
) =>
googleapis_auth.clientViaRefreshToken(
googleapis_auth.ClientId(clientId, clientSecret),
refreshToken,
scopes,
),
_ => googleapis_auth.clientViaApplicationDefaultCredentials(
scopes: scopes,
),
Expand Down
6 changes: 5 additions & 1 deletion packages/dart_firebase_admin/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ dependencies:
google_cloud_firestore: ^0.1.0
google_cloud_storage: ^0.6.0
googleapis: ^16.0.0
googleapis_auth: ^2.2.0
googleapis_auth:
git:
url: https://github.com/google/googleapis.dart.git
path: googleapis_auth
ref: c3fa07da0841ba75493c59b64aa0aa901541957e
googleapis_beta: ^9.0.0
http: ^1.6.0
intl: ^0.20.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,65 @@ void main() {
);
});

group('_createDefaultClient – refresh token path', () {
final refreshTokenFile =
Platform.environment['FIREBASE_REFRESH_TOKEN_CREDENTIALS'];

test(
'creates an authenticated client via refresh token credential',
() {
return runZoned(() async {
final credential = Credential.fromRefreshToken(
File(refreshTokenFile!),
);

final app = FirebaseApp.initializeApp(
name: 'rt-client-${DateTime.now().microsecondsSinceEpoch}',
options: AppOptions(projectId: projectId, credential: credential),
);

try {
final client = await app.client;
expect(client, isNotNull);
} finally {
await app.close();
}
}, zoneValues: {envSymbol: prodEnv()});
},
skip: refreshTokenFile != null
? false
: 'Requires FIREBASE_REFRESH_TOKEN_CREDENTIALS to be set '
'(path to a refresh token JSON file, e.g. '
'~/.config/gcloud/application_default_credentials.json)',
timeout: const Timeout(Duration(seconds: 30)),
);

test(
'SDK-created refresh token client is closed when app.close() is called',
() {
return runZoned(() async {
final credential = Credential.fromRefreshToken(
File(refreshTokenFile!),
);

final app = FirebaseApp.initializeApp(
name: 'rt-close-${DateTime.now().microsecondsSinceEpoch}',
options: AppOptions(projectId: projectId, credential: credential),
);

await app.client;
await app.close();

expect(app.isDeleted, isTrue);
}, zoneValues: {envSymbol: prodEnv()});
},
skip: refreshTokenFile != null
? false
: 'Requires FIREBASE_REFRESH_TOKEN_CREDENTIALS to be set',
timeout: const Timeout(Duration(seconds: 30)),
);
});

group('getProjectId – computeProjectId fallback', () {
test(
'falls back to computeProjectId() when no projectId source is configured',
Expand Down
Loading
Loading