@@ -23,6 +23,8 @@ const envSymbol = #_envSymbol;
2323/// - [Credential.fromServiceAccount] - For service account JSON files
2424/// - [Credential.fromServiceAccountParams] - For individual service account parameters
2525/// - [Credential.fromApplicationDefaultCredentials] - For Application Default Credentials (ADC)
26+ /// - [Credential.fromRefreshToken] - For OAuth2 refresh token JSON files
27+ /// - [Credential.fromRefreshTokenParams] - For individual OAuth2 refresh token parameters
2628///
2729/// The credential is used to authenticate all API calls made by the Admin SDK.
2830sealed class Credential {
@@ -115,6 +117,124 @@ sealed class Credential {
115117 }
116118 }
117119
120+ /// Creates a credential from an OAuth2 refresh token JSON file.
121+ ///
122+ /// The file must contain:
123+ /// - `client_id` : The OAuth2 client ID
124+ /// - `client_secret` : The OAuth2 client secret
125+ /// - `refresh_token` : The refresh token
126+ /// - `type` : The credential type (typically `"authorized_user"` )
127+ ///
128+ /// You can obtain a refresh token JSON file by running:
129+ /// ```
130+ /// gcloud auth application-default login
131+ /// ```
132+ /// which writes credentials to `~/.config/gcloud/application_default_credentials.json` .
133+ ///
134+ /// Example:
135+ /// ```dart
136+ /// final credential = Credential.fromRefreshToken(
137+ /// File('path/to/refresh_token.json'),
138+ /// );
139+ /// ```
140+ factory Credential .fromRefreshToken (File refreshTokenFile) {
141+ final String raw;
142+ try {
143+ raw = refreshTokenFile.readAsStringSync ();
144+ } on IOException catch (e) {
145+ throw FirebaseAppException (
146+ AppErrorCode .invalidCredential,
147+ 'Failed to read refresh token file: $e ' ,
148+ );
149+ }
150+
151+ final Object ? json;
152+ try {
153+ json = jsonDecode (raw);
154+ } on FormatException catch (e) {
155+ throw FirebaseAppException (
156+ AppErrorCode .invalidCredential,
157+ 'Failed to parse refresh token JSON: ${e .message }' ,
158+ );
159+ }
160+
161+ if (json case {
162+ 'client_id' : final String clientId,
163+ 'client_secret' : final String clientSecret,
164+ 'refresh_token' : final String refreshToken,
165+ 'type' : final String type,
166+ } when clientId.isNotEmpty &&
167+ clientSecret.isNotEmpty &&
168+ refreshToken.isNotEmpty &&
169+ type.isNotEmpty) {
170+ return RefreshTokenCredential ._(
171+ clientId: clientId,
172+ clientSecret: clientSecret,
173+ refreshToken: refreshToken,
174+ );
175+ }
176+
177+ throw FirebaseAppException (
178+ AppErrorCode .invalidCredential,
179+ 'Refresh token file must contain non-empty string fields: '
180+ '"client_id", "client_secret", "refresh_token", and "type".' ,
181+ );
182+ }
183+
184+ /// Creates a credential from individual OAuth2 refresh token parameters.
185+ ///
186+ /// Parameters:
187+ /// - [clientId] : The OAuth2 client ID
188+ /// - [clientSecret] : The OAuth2 client secret
189+ /// - [refreshToken] : The refresh token
190+ /// - [type] : The credential type (typically `"authorized_user"` )
191+ ///
192+ /// Example:
193+ /// ```dart
194+ /// final credential = Credential.fromRefreshTokenParams(
195+ /// clientId: 'my-client-id',
196+ /// clientSecret: 'my-client-secret',
197+ /// refreshToken: 'my-refresh-token',
198+ /// type: 'authorized_user',
199+ /// );
200+ /// ```
201+ factory Credential .fromRefreshTokenParams ({
202+ required String clientId,
203+ required String clientSecret,
204+ required String refreshToken,
205+ required String type,
206+ }) {
207+ if (clientId.isEmpty) {
208+ throw FirebaseAppException (
209+ AppErrorCode .invalidCredential,
210+ 'Refresh token must contain a non-empty "client_id".' ,
211+ );
212+ }
213+ if (clientSecret.isEmpty) {
214+ throw FirebaseAppException (
215+ AppErrorCode .invalidCredential,
216+ 'Refresh token must contain a non-empty "client_secret".' ,
217+ );
218+ }
219+ if (refreshToken.isEmpty) {
220+ throw FirebaseAppException (
221+ AppErrorCode .invalidCredential,
222+ 'Refresh token must contain a non-empty "refresh_token".' ,
223+ );
224+ }
225+ if (type.isEmpty) {
226+ throw FirebaseAppException (
227+ AppErrorCode .invalidCredential,
228+ 'Refresh token must contain a non-empty "type".' ,
229+ );
230+ }
231+ return RefreshTokenCredential ._(
232+ clientId: clientId,
233+ clientSecret: clientSecret,
234+ refreshToken: refreshToken,
235+ );
236+ }
237+
118238 /// Private constructor for sealed class.
119239 Credential ._();
120240
@@ -170,6 +290,64 @@ final class ServiceAccountCredential extends Credential {
170290 String ? get serviceAccountId => _serviceAccountCredentials.email;
171291}
172292
293+ /// OAuth2 refresh token credentials for Firebase Admin SDK.
294+ ///
295+ /// Uses a refresh token to obtain and automatically refresh access tokens.
296+ /// Obtain a refresh token file by running `gcloud auth application-default login` .
297+ @internal
298+ final class RefreshTokenCredential extends Credential {
299+ RefreshTokenCredential ._({
300+ required this .clientId,
301+ required this .clientSecret,
302+ required this .refreshToken,
303+ }) : super ._();
304+
305+ /// The OAuth2 client ID.
306+ final String clientId;
307+
308+ /// The OAuth2 client secret.
309+ final String clientSecret;
310+
311+ /// The OAuth2 refresh token.
312+ final String refreshToken;
313+
314+ @override
315+ googleapis_auth.ServiceAccountCredentials ? get serviceAccountCredentials =>
316+ null ;
317+
318+ @override
319+ String ? get serviceAccountId => null ;
320+
321+ // TODO: move this into googleapis_auth as clientViaRefreshToken
322+ /// Creates an auto-refreshing authenticated HTTP client for [scopes] .
323+ ///
324+ /// An optional [baseClient] can be provided for testing. When omitted, a
325+ /// plain [Client] is used.
326+ @internal
327+ Future <googleapis_auth.AutoRefreshingAuthClient > createAuthClient (
328+ List <String > scopes, {
329+ Client ? baseClient,
330+ }) async {
331+ final id = googleapis_auth.ClientId (clientId, clientSecret);
332+ // Deliberately expired — forces a token exchange on the first API call.
333+ final expiredToken = googleapis_auth.AccessToken (
334+ 'Bearer' ,
335+ '' ,
336+ DateTime .fromMillisecondsSinceEpoch (0 , isUtc: true ),
337+ );
338+ final credentials = googleapis_auth.AccessCredentials (
339+ expiredToken,
340+ refreshToken,
341+ scopes,
342+ );
343+ return googleapis_auth.autoRefreshingClient (
344+ id,
345+ credentials,
346+ baseClient ?? Client (),
347+ );
348+ }
349+ }
350+
173351/// Application Default Credentials for Firebase Admin SDK.
174352///
175353/// Uses Google Application Default Credentials (ADC) to automatically discover
0 commit comments