Skip to content

Commit 99e39bd

Browse files
committed
feat(auth): create dedicated FirebaseAuthenticator service
Extracts the Firebase JWT signing and token exchange logic from `app_dependencies.dart` into a new, dedicated `FirebaseAuthenticator` class. This refactoring improves separation of concerns, enhances readability of the dependency setup, and makes the authentication flow testable in isolation.
1 parent b8e03b2 commit 99e39bd

File tree

1 file changed

+78
-0
lines changed

1 file changed

+78
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'package:core/core.dart';
2+
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
3+
import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart';
4+
import 'package:http_client/http_client.dart';
5+
import 'package:logging/logging.dart';
6+
7+
/// An abstract interface for a service that provides Firebase access tokens.
8+
abstract class IFirebaseAuthenticator {
9+
/// Retrieves a short-lived OAuth2 access token for Firebase.
10+
Future<String?> getAccessToken();
11+
}
12+
13+
/// A concrete implementation of [IFirebaseAuthenticator] that uses a
14+
/// two-legged OAuth flow to obtain an access token from Google.
15+
class FirebaseAuthenticator implements IFirebaseAuthenticator {
16+
/// Creates an instance of [FirebaseAuthenticator].
17+
FirebaseAuthenticator({required Logger log}) : _log = log {
18+
// This internal HttpClient is used exclusively for the token exchange.
19+
// It does not have an auth interceptor, which is crucial to prevent
20+
// an infinite loop.
21+
_tokenClient = HttpClient(
22+
baseUrl: 'https://oauth2.googleapis.com',
23+
tokenProvider: () async => null,
24+
);
25+
}
26+
27+
final Logger _log;
28+
late final HttpClient _tokenClient;
29+
30+
@override
31+
Future<String?> getAccessToken() async {
32+
_log.info('Requesting new Firebase access token...');
33+
try {
34+
// Step 1: Create and sign the JWT.
35+
final pem = EnvironmentConfig.firebasePrivateKey.replaceAll(r'\n', '\n');
36+
final privateKey = RSAPrivateKey(pem);
37+
final jwt = JWT(
38+
{'scope': 'https://www.googleapis.com/auth/cloud-platform'},
39+
issuer: EnvironmentConfig.firebaseClientEmail,
40+
audience: Audience.one('https://oauth2.googleapis.com/token'),
41+
);
42+
final signedToken = jwt.sign(
43+
privateKey,
44+
algorithm: JWTAlgorithm.RS256,
45+
expiresIn: const Duration(minutes: 5),
46+
);
47+
_log.finer('Successfully signed JWT for token exchange.');
48+
49+
// Step 2: Exchange the JWT for an access token.
50+
final response = await _tokenClient.post<Map<String, dynamic>>(
51+
'/token',
52+
data: {
53+
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
54+
'assertion': signedToken,
55+
},
56+
);
57+
58+
final accessToken = response['access_token'] as String?;
59+
if (accessToken == null) {
60+
_log.severe('Google OAuth response did not contain an access_token.');
61+
throw const OperationFailedException(
62+
'Could not retrieve Firebase access token.',
63+
);
64+
}
65+
_log.info('Successfully retrieved new Firebase access token.');
66+
return accessToken;
67+
} on HttpException {
68+
// Re-throw known HTTP exceptions directly.
69+
rethrow;
70+
} catch (e, s) {
71+
_log.severe('Error during Firebase token exchange: $e', e, s);
72+
// Wrap other errors in a standard exception.
73+
throw OperationFailedException(
74+
'Failed to authenticate with Firebase: $e',
75+
);
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)