From 993c86e6aff25ef6b1115b74ae3fb9c5a39ecb1b Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 1 Apr 2026 10:15:59 +0100 Subject: [PATCH 1/4] feat: implement OAuth2 refresh token credential support --- .../lib/dart_firebase_admin.dart | 1 + .../lib/src/app/credential.dart | 178 ++++++++++++++++ .../lib/src/app/firebase_app.dart | 1 + .../app/firebase_app_prod_test.dart | 58 ++++++ .../app/refresh_token_credential_test.dart | 195 ++++++++++++++++++ scripts/coverage.sh | 4 + 6 files changed, 437 insertions(+) create mode 100644 packages/dart_firebase_admin/test/unit/app/refresh_token_credential_test.dart diff --git a/packages/dart_firebase_admin/lib/dart_firebase_admin.dart b/packages/dart_firebase_admin/lib/dart_firebase_admin.dart index bff9e7bf..b7e5e14f 100644 --- a/packages/dart_firebase_admin/lib/dart_firebase_admin.dart +++ b/packages/dart_firebase_admin/lib/dart_firebase_admin.dart @@ -22,5 +22,6 @@ export 'src/app.dart' FirebaseService, FirebaseServiceType, FirebaseUserAgentClient, + RefreshTokenCredential, ServiceAccountCredential, envSymbol; diff --git a/packages/dart_firebase_admin/lib/src/app/credential.dart b/packages/dart_firebase_admin/lib/src/app/credential.dart index 43df6368..12d76fa5 100644 --- a/packages/dart_firebase_admin/lib/src/app/credential.dart +++ b/packages/dart_firebase_admin/lib/src/app/credential.dart @@ -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 { @@ -115,6 +117,124 @@ 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, + required String type, + }) { + 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._(); @@ -170,6 +290,64 @@ 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; + + // TODO: move this into googleapis_auth as clientViaRefreshToken + /// Creates an auto-refreshing authenticated HTTP client for [scopes]. + /// + /// An optional [baseClient] can be provided for testing. When omitted, a + /// plain [Client] is used. + @internal + Future createAuthClient( + List scopes, { + Client? baseClient, + }) async { + final id = googleapis_auth.ClientId(clientId, clientSecret); + // Deliberately expired — forces a token exchange on the first API call. + final expiredToken = googleapis_auth.AccessToken( + 'Bearer', + '', + DateTime.fromMillisecondsSinceEpoch(0, isUtc: true), + ); + final credentials = googleapis_auth.AccessCredentials( + expiredToken, + refreshToken, + scopes, + ); + return googleapis_auth.autoRefreshingClient( + id, + credentials, + baseClient ?? Client(), + ); + } +} + /// Application Default Credentials for Firebase Admin SDK. /// /// Uses Google Application Default Credentials (ADC) to automatically discover diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart index c58d34ee..83f3cf86 100644 --- a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart +++ b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart @@ -127,6 +127,7 @@ class FirebaseApp { serviceAccountCredentials, scopes, ), + RefreshTokenCredential() => credential.createAuthClient(scopes), _ => googleapis_auth.clientViaApplicationDefaultCredentials( scopes: scopes, ), diff --git a/packages/dart_firebase_admin/test/integration/app/firebase_app_prod_test.dart b/packages/dart_firebase_admin/test/integration/app/firebase_app_prod_test.dart index 9abd6f6a..38def4c1 100644 --- a/packages/dart_firebase_admin/test/integration/app/firebase_app_prod_test.dart +++ b/packages/dart_firebase_admin/test/integration/app/firebase_app_prod_test.dart @@ -99,6 +99,64 @@ 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', diff --git a/packages/dart_firebase_admin/test/unit/app/refresh_token_credential_test.dart b/packages/dart_firebase_admin/test/unit/app/refresh_token_credential_test.dart new file mode 100644 index 00000000..850f440a --- /dev/null +++ b/packages/dart_firebase_admin/test/unit/app/refresh_token_credential_test.dart @@ -0,0 +1,195 @@ +// Copyright 2026 Firebase +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; + +import 'package:dart_firebase_admin/src/app.dart'; +import 'package:file/memory.dart'; +import 'package:test/test.dart'; + +void main() { + group('Credential.fromRefreshToken', () { + test('throws if file is missing', () { + final fs = MemoryFileSystem.test(); + expect( + () => Credential.fromRefreshToken(fs.file('refresh_token.json')), + throwsA(isA()), + ); + }); + + test('throws if file content is not valid JSON', () { + final fs = MemoryFileSystem.test(); + fs.file('refresh_token.json').writeAsStringSync('not-json'); + expect( + () => Credential.fromRefreshToken(fs.file('refresh_token.json')), + throwsA(isA()), + ); + }); + + test('throws if client_id is missing', () { + final fs = MemoryFileSystem.test(); + fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ + 'client_secret': 'secret', + 'refresh_token': 'token', + 'type': 'authorized_user', + })); + expect( + () => Credential.fromRefreshToken(fs.file('refresh_token.json')), + throwsA(isA()), + ); + }); + + test('throws if client_secret is missing', () { + final fs = MemoryFileSystem.test(); + fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ + 'client_id': 'id', + 'refresh_token': 'token', + 'type': 'authorized_user', + })); + expect( + () => Credential.fromRefreshToken(fs.file('refresh_token.json')), + throwsA(isA()), + ); + }); + + test('throws if refresh_token is missing', () { + final fs = MemoryFileSystem.test(); + fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ + 'client_id': 'id', + 'client_secret': 'secret', + 'type': 'authorized_user', + })); + expect( + () => Credential.fromRefreshToken(fs.file('refresh_token.json')), + throwsA(isA()), + ); + }); + + test('throws if type is missing', () { + final fs = MemoryFileSystem.test(); + fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ + 'client_id': 'id', + 'client_secret': 'secret', + 'refresh_token': 'token', + })); + expect( + () => Credential.fromRefreshToken(fs.file('refresh_token.json')), + throwsA(isA()), + ); + }); + + test('throws if any field is an empty string', () { + final fs = MemoryFileSystem.test(); + fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ + 'client_id': '', + 'client_secret': 'secret', + 'refresh_token': 'token', + 'type': 'authorized_user', + })); + expect( + () => Credential.fromRefreshToken(fs.file('refresh_token.json')), + throwsA(isA()), + ); + }); + + test('returns RefreshTokenCredential for valid file', () { + final fs = MemoryFileSystem.test(); + fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ + 'client_id': 'test-id', + 'client_secret': 'test-secret', + 'refresh_token': 'test-refresh-token', + 'type': 'authorized_user', + })); + + final credential = Credential.fromRefreshToken( + fs.file('refresh_token.json'), + ); + + expect(credential, isA()); + final rt = credential as RefreshTokenCredential; + expect(rt.clientId, 'test-id'); + expect(rt.clientSecret, 'test-secret'); + expect(rt.refreshToken, 'test-refresh-token'); + expect(rt.serviceAccountCredentials, isNull); + expect(rt.serviceAccountId, isNull); + }); + }); + + group('Credential.fromRefreshTokenParams', () { + test('throws if clientId is empty', () { + expect( + () => Credential.fromRefreshTokenParams( + clientId: '', + clientSecret: 'secret', + refreshToken: 'token', + type: 'authorized_user', + ), + throwsA(isA()), + ); + }); + + test('throws if clientSecret is empty', () { + expect( + () => Credential.fromRefreshTokenParams( + clientId: 'id', + clientSecret: '', + refreshToken: 'token', + type: 'authorized_user', + ), + throwsA(isA()), + ); + }); + + test('throws if refreshToken is empty', () { + expect( + () => Credential.fromRefreshTokenParams( + clientId: 'id', + clientSecret: 'secret', + refreshToken: '', + type: 'authorized_user', + ), + throwsA(isA()), + ); + }); + + test('throws if type is empty', () { + expect( + () => Credential.fromRefreshTokenParams( + clientId: 'id', + clientSecret: 'secret', + refreshToken: 'token', + type: '', + ), + throwsA(isA()), + ); + }); + + test('returns RefreshTokenCredential for valid params', () { + final credential = Credential.fromRefreshTokenParams( + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + refreshToken: 'my-refresh-token', + type: 'authorized_user', + ); + + expect(credential, isA()); + final rt = credential as RefreshTokenCredential; + expect(rt.clientId, 'my-client-id'); + expect(rt.clientSecret, 'my-client-secret'); + expect(rt.refreshToken, 'my-refresh-token'); + expect(rt.serviceAccountCredentials, isNull); + expect(rt.serviceAccountId, isNull); + }); + }); +} diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 38bc0b23..cade9a0b 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -7,6 +7,10 @@ set -e # export GOOGLE_APPLICATION_CREDENTIALS=service-account-key.json # export RUN_PROD_TESTS=true # +# To also run the refresh token credential integration tests, set: +# export FIREBASE_REFRESH_TOKEN_CREDENTIALS=~/.config/gcloud/application_default_credentials.json +# (run `gcloud auth application-default login` first if the file doesn't exist) +# # RUN_PROD_TESTS is intentionally never set in CI to avoid quota-heavy tests running there. # WIF tests (gated by hasWifEnv) still run in CI via the google-github-actions/auth step. From d2d35403e58029b9eac23f0faec3c687e1c93b25 Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 1 Apr 2026 10:18:20 +0100 Subject: [PATCH 2/4] fix: use default authorized_user key for refresh token credential type --- .../lib/src/app/credential.dart | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/app/credential.dart b/packages/dart_firebase_admin/lib/src/app/credential.dart index 12d76fa5..bb00bb03 100644 --- a/packages/dart_firebase_admin/lib/src/app/credential.dart +++ b/packages/dart_firebase_admin/lib/src/app/credential.dart @@ -158,15 +158,17 @@ sealed class Credential { ); } - 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) { + 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, @@ -202,7 +204,7 @@ sealed class Credential { required String clientId, required String clientSecret, required String refreshToken, - required String type, + String type = 'authorized_user', }) { if (clientId.isEmpty) { throw FirebaseAppException( From a622e3a8a61d4c046560d60d3f20270f8b6054ab Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 1 Apr 2026 10:18:49 +0100 Subject: [PATCH 3/4] chore: fix lint --- .../app/firebase_app_prod_test.dart | 7 +- .../app/refresh_token_credential_test.dart | 88 ++++++++++++------- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/packages/dart_firebase_admin/test/integration/app/firebase_app_prod_test.dart b/packages/dart_firebase_admin/test/integration/app/firebase_app_prod_test.dart index 38def4c1..2979e09f 100644 --- a/packages/dart_firebase_admin/test/integration/app/firebase_app_prod_test.dart +++ b/packages/dart_firebase_admin/test/integration/app/firebase_app_prod_test.dart @@ -100,7 +100,8 @@ void main() { }); group('_createDefaultClient – refresh token path', () { - final refreshTokenFile = Platform.environment['FIREBASE_REFRESH_TOKEN_CREDENTIALS']; + final refreshTokenFile = + Platform.environment['FIREBASE_REFRESH_TOKEN_CREDENTIALS']; test( 'creates an authenticated client via refresh token credential', @@ -126,8 +127,8 @@ void main() { 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)', + '(path to a refresh token JSON file, e.g. ' + '~/.config/gcloud/application_default_credentials.json)', timeout: const Timeout(Duration(seconds: 30)), ); diff --git a/packages/dart_firebase_admin/test/unit/app/refresh_token_credential_test.dart b/packages/dart_firebase_admin/test/unit/app/refresh_token_credential_test.dart index 850f440a..ad5ab5a5 100644 --- a/packages/dart_firebase_admin/test/unit/app/refresh_token_credential_test.dart +++ b/packages/dart_firebase_admin/test/unit/app/refresh_token_credential_test.dart @@ -39,11 +39,15 @@ void main() { test('throws if client_id is missing', () { final fs = MemoryFileSystem.test(); - fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ - 'client_secret': 'secret', - 'refresh_token': 'token', - 'type': 'authorized_user', - })); + fs + .file('refresh_token.json') + .writeAsStringSync( + jsonEncode({ + 'client_secret': 'secret', + 'refresh_token': 'token', + 'type': 'authorized_user', + }), + ); expect( () => Credential.fromRefreshToken(fs.file('refresh_token.json')), throwsA(isA()), @@ -52,11 +56,15 @@ void main() { test('throws if client_secret is missing', () { final fs = MemoryFileSystem.test(); - fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ - 'client_id': 'id', - 'refresh_token': 'token', - 'type': 'authorized_user', - })); + fs + .file('refresh_token.json') + .writeAsStringSync( + jsonEncode({ + 'client_id': 'id', + 'refresh_token': 'token', + 'type': 'authorized_user', + }), + ); expect( () => Credential.fromRefreshToken(fs.file('refresh_token.json')), throwsA(isA()), @@ -65,11 +73,15 @@ void main() { test('throws if refresh_token is missing', () { final fs = MemoryFileSystem.test(); - fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ - 'client_id': 'id', - 'client_secret': 'secret', - 'type': 'authorized_user', - })); + fs + .file('refresh_token.json') + .writeAsStringSync( + jsonEncode({ + 'client_id': 'id', + 'client_secret': 'secret', + 'type': 'authorized_user', + }), + ); expect( () => Credential.fromRefreshToken(fs.file('refresh_token.json')), throwsA(isA()), @@ -78,11 +90,15 @@ void main() { test('throws if type is missing', () { final fs = MemoryFileSystem.test(); - fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ - 'client_id': 'id', - 'client_secret': 'secret', - 'refresh_token': 'token', - })); + fs + .file('refresh_token.json') + .writeAsStringSync( + jsonEncode({ + 'client_id': 'id', + 'client_secret': 'secret', + 'refresh_token': 'token', + }), + ); expect( () => Credential.fromRefreshToken(fs.file('refresh_token.json')), throwsA(isA()), @@ -91,12 +107,16 @@ void main() { test('throws if any field is an empty string', () { final fs = MemoryFileSystem.test(); - fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ - 'client_id': '', - 'client_secret': 'secret', - 'refresh_token': 'token', - 'type': 'authorized_user', - })); + fs + .file('refresh_token.json') + .writeAsStringSync( + jsonEncode({ + 'client_id': '', + 'client_secret': 'secret', + 'refresh_token': 'token', + 'type': 'authorized_user', + }), + ); expect( () => Credential.fromRefreshToken(fs.file('refresh_token.json')), throwsA(isA()), @@ -105,12 +125,16 @@ void main() { test('returns RefreshTokenCredential for valid file', () { final fs = MemoryFileSystem.test(); - fs.file('refresh_token.json').writeAsStringSync(jsonEncode({ - 'client_id': 'test-id', - 'client_secret': 'test-secret', - 'refresh_token': 'test-refresh-token', - 'type': 'authorized_user', - })); + fs + .file('refresh_token.json') + .writeAsStringSync( + jsonEncode({ + 'client_id': 'test-id', + 'client_secret': 'test-secret', + 'refresh_token': 'test-refresh-token', + 'type': 'authorized_user', + }), + ); final credential = Credential.fromRefreshToken( fs.file('refresh_token.json'), From d2d612d0f47e371d560a8999a80e664a8d27da30 Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 8 Apr 2026 11:51:31 +0100 Subject: [PATCH 4/4] wip update refresh token credential implementation and adjust dependency for googleapis_auth --- .../lib/src/app/credential.dart | 29 ------------------- .../lib/src/app/firebase_app.dart | 11 ++++++- packages/dart_firebase_admin/pubspec.yaml | 6 +++- packages/google_cloud_firestore/pubspec.yaml | 6 +++- pubspec.yaml | 7 +++++ 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/app/credential.dart b/packages/dart_firebase_admin/lib/src/app/credential.dart index bb00bb03..2831325b 100644 --- a/packages/dart_firebase_admin/lib/src/app/credential.dart +++ b/packages/dart_firebase_admin/lib/src/app/credential.dart @@ -319,35 +319,6 @@ final class RefreshTokenCredential extends Credential { @override String? get serviceAccountId => null; - - // TODO: move this into googleapis_auth as clientViaRefreshToken - /// Creates an auto-refreshing authenticated HTTP client for [scopes]. - /// - /// An optional [baseClient] can be provided for testing. When omitted, a - /// plain [Client] is used. - @internal - Future createAuthClient( - List scopes, { - Client? baseClient, - }) async { - final id = googleapis_auth.ClientId(clientId, clientSecret); - // Deliberately expired — forces a token exchange on the first API call. - final expiredToken = googleapis_auth.AccessToken( - 'Bearer', - '', - DateTime.fromMillisecondsSinceEpoch(0, isUtc: true), - ); - final credentials = googleapis_auth.AccessCredentials( - expiredToken, - refreshToken, - scopes, - ); - return googleapis_auth.autoRefreshingClient( - id, - credentials, - baseClient ?? Client(), - ); - } } /// Application Default Credentials for Firebase Admin SDK. diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart index 83f3cf86..320b0241 100644 --- a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart +++ b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart @@ -127,7 +127,16 @@ class FirebaseApp { serviceAccountCredentials, scopes, ), - RefreshTokenCredential() => credential.createAuthClient(scopes), + RefreshTokenCredential( + :final clientId, + :final clientSecret, + :final refreshToken, + ) => + googleapis_auth.clientViaRefreshToken( + googleapis_auth.ClientId(clientId, clientSecret), + refreshToken, + scopes, + ), _ => googleapis_auth.clientViaApplicationDefaultCredentials( scopes: scopes, ), diff --git a/packages/dart_firebase_admin/pubspec.yaml b/packages/dart_firebase_admin/pubspec.yaml index 15dd6b76..bb3b0d13 100644 --- a/packages/dart_firebase_admin/pubspec.yaml +++ b/packages/dart_firebase_admin/pubspec.yaml @@ -19,7 +19,11 @@ dependencies: google_cloud_firestore: 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 diff --git a/packages/google_cloud_firestore/pubspec.yaml b/packages/google_cloud_firestore/pubspec.yaml index 4b246635..7b8ffc04 100644 --- a/packages/google_cloud_firestore/pubspec.yaml +++ b/packages/google_cloud_firestore/pubspec.yaml @@ -11,7 +11,11 @@ dependencies: collection: ^1.19.1 google_cloud: ^0.4.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 http: ^1.6.0 intl: ^0.20.0 meta: ^1.18.1 diff --git a/pubspec.yaml b/pubspec.yaml index e28814ec..906763c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,13 @@ workspace: - packages/dart_firebase_admin/example - packages/dart_firebase_admin/example_server_app +dependency_overrides: + googleapis_auth: + git: + url: https://github.com/google/googleapis.dart.git + path: googleapis_auth + ref: c3fa07da0841ba75493c59b64aa0aa901541957e + melos: scripts: docs: