1
+ import 'dart:convert' ; // For potential base64 decoding if needed
2
+
1
3
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' ;
2
4
import 'package:ht_api/src/services/auth_token_service.dart' ;
5
+ // Import the blacklist service
6
+ import 'package:ht_api/src/services/token_blacklist_service.dart' ;
3
7
import 'package:ht_data_repository/ht_data_repository.dart' ;
4
8
import 'package:ht_shared/ht_shared.dart' ;
5
9
import 'package:uuid/uuid.dart' ;
6
10
7
11
/// {@template jwt_auth_token_service}
8
12
/// An implementation of [AuthTokenService] using JSON Web Tokens (JWT).
9
13
///
10
- /// Handles the creation (signing) and validation (verification) of JWTs
11
- /// for user authentication .
14
+ /// Handles the creation (signing) and validation (verification) of JWTs,
15
+ /// including support for token invalidation via blacklisting .
12
16
/// {@endtemplate}
13
17
class JwtAuthTokenService implements AuthTokenService {
14
18
/// {@macro jwt_auth_token_service}
15
19
///
16
- /// Requires an [HtDataRepository<User>] to fetch user details after
17
- /// validating the token's subject claim.
18
- /// Also requires a [Uuid] generator for creating unique JWT IDs (jti).
20
+ /// Requires:
21
+ /// - [userRepository] : To fetch user details after validating the token's
22
+ /// subject claim.
23
+ /// - [blacklistService] : To manage the blacklist of invalidated tokens.
24
+ /// - [uuidGenerator] : For creating unique JWT IDs (jti).
19
25
const JwtAuthTokenService ({
20
26
required HtDataRepository <User > userRepository,
27
+ required TokenBlacklistService blacklistService,
21
28
required Uuid uuidGenerator,
22
29
}) : _userRepository = userRepository,
30
+ _blacklistService = blacklistService,
23
31
_uuid = uuidGenerator;
24
32
25
33
final HtDataRepository <User > _userRepository;
34
+ final TokenBlacklistService _blacklistService;
26
35
final Uuid _uuid;
27
36
28
37
// --- Configuration ---
@@ -89,7 +98,30 @@ class JwtAuthTokenService implements AuthTokenService {
89
98
final jwt = JWT .verify (token, SecretKey (_secretKey));
90
99
print ('[validateToken] Token verified. Payload: ${jwt .payload }' );
91
100
92
- // Extract user ID from the subject claim
101
+ // --- Blacklist Check ---
102
+ // Extract the JWT ID (jti) claim
103
+ final jti = jwt.payload['jti' ] as String ? ;
104
+ if (jti == null || jti.isEmpty) {
105
+ print (
106
+ '[validateToken] Token validation failed: Missing or empty "jti" claim.' );
107
+ // Throw specific exception for malformed token
108
+ throw const BadRequestException (
109
+ 'Malformed token: Missing or empty JWT ID (jti) claim.' ,
110
+ );
111
+ }
112
+
113
+ print ('[validateToken] Checking blacklist for jti: $jti ' );
114
+ final isBlacklisted = await _blacklistService.isBlacklisted (jti);
115
+ if (isBlacklisted) {
116
+ print (
117
+ '[validateToken] Token validation failed: Token is blacklisted (jti: $jti ).' );
118
+ // Throw specific exception for blacklisted token
119
+ throw const UnauthorizedException ('Token has been invalidated.' );
120
+ }
121
+ print ('[validateToken] Token is not blacklisted (jti: $jti ).' );
122
+ // --- End Blacklist Check ---
123
+
124
+ // Extract user ID from the subject claim ('sub')
93
125
final subClaim = jwt.payload['sub' ];
94
126
print (
95
127
'[validateToken] Extracted "sub" claim: $subClaim '
@@ -104,12 +136,11 @@ class JwtAuthTokenService implements AuthTokenService {
104
136
'[validateToken] "sub" claim successfully cast to String: $userId ' ,
105
137
);
106
138
} else if (subClaim != null ) {
139
+ // Treat non-string sub as an error
107
140
print (
108
- '[validateToken] WARNING : "sub" claim is not a String. '
109
- 'Attempting toString( ).' ,
141
+ '[validateToken] ERROR : "sub" claim is not a String '
142
+ '(Type: ${ subClaim . runtimeType } ).' ,
110
143
);
111
- // Handle potential non-string types if necessary, or throw error
112
- // For now, let's treat non-string sub as an error
113
144
throw BadRequestException (
114
145
'Malformed token: "sub" claim is not a String '
115
146
'(Type: ${subClaim .runtimeType }).' ,
@@ -135,8 +166,8 @@ class JwtAuthTokenService implements AuthTokenService {
135
166
return user;
136
167
} on JWTExpiredException catch (e, s) {
137
168
print ('[validateToken] CATCH JWTExpiredException: Token expired. $e \n $s ' );
138
- // Throw specific exception for expired token
139
- throw const UnauthorizedException ( 'Token expired.' ) ;
169
+ // Let the specific UnauthorizedException for expiry propagate
170
+ rethrow ;
140
171
} on JWTInvalidException catch (e, s) {
141
172
print (
142
173
'[validateToken] CATCH JWTInvalidException: Invalid token. '
@@ -145,7 +176,7 @@ class JwtAuthTokenService implements AuthTokenService {
145
176
// Throw specific exception for invalid token signature/format
146
177
throw UnauthorizedException ('Invalid token: ${e .message }' );
147
178
} on JWTException catch (e, s) {
148
- // Use JWTException as the general catch-all
179
+ // Use JWTException as the general catch-all for other JWT issues
149
180
print (
150
181
'[validateToken] CATCH JWTException: General JWT error. '
151
182
'Reason: ${e .message }\n $s ' ,
@@ -154,11 +185,12 @@ class JwtAuthTokenService implements AuthTokenService {
154
185
throw UnauthorizedException ('Invalid token: ${e .message }' );
155
186
} on HtHttpException catch (e, s) {
156
187
// Handle errors from the user repository (e.g., user not found)
188
+ // or blacklist check (if it threw HtHttpException)
157
189
print (
158
- '[validateToken] CATCH HtHttpException: Error fetching user . '
190
+ '[validateToken] CATCH HtHttpException: Error during validation . '
159
191
'Type: ${e .runtimeType }, Message: $e \n $s ' ,
160
192
);
161
- // Re-throw repository exceptions directly for the error handler
193
+ // Re-throw repository/blacklist exceptions directly
162
194
rethrow ;
163
195
} catch (e, s) {
164
196
// Catch unexpected errors during validation
@@ -169,4 +201,71 @@ class JwtAuthTokenService implements AuthTokenService {
169
201
);
170
202
}
171
203
}
204
+
205
+ @override
206
+ Future <void > invalidateToken (String token) async {
207
+ print ('[invalidateToken] Attempting to invalidate token...' );
208
+ try {
209
+ // 1. Verify the token signature FIRST, but ignore expiry for blacklisting
210
+ // We want to blacklist even if it's already expired, to be safe.
211
+ print ('[invalidateToken] Verifying token signature (ignoring expiry)...' );
212
+ final jwt = JWT .verify (
213
+ token,
214
+ SecretKey (_secretKey),
215
+ checkExpiresIn: false , // IMPORTANT: Don't fail if expired here
216
+ checkHeaderType: true , // Keep other standard checks
217
+ // checkIssuedAt: true, // This parameter doesn't exist
218
+ );
219
+ print ('[invalidateToken] Token signature verified.' );
220
+
221
+ // 2. Extract JTI (JWT ID)
222
+ final jti = jwt.payload['jti' ] as String ? ;
223
+ if (jti == null || jti.isEmpty) {
224
+ print ('[invalidateToken] Failed: Missing or empty "jti" claim.' );
225
+ throw const InvalidInputException (
226
+ 'Cannot invalidate token: Missing or empty JWT ID (jti) claim.' ,
227
+ );
228
+ }
229
+ print ('[invalidateToken] Extracted jti: $jti ' );
230
+
231
+ // 3. Extract Expiry Time (exp)
232
+ final expClaim = jwt.payload['exp' ];
233
+ if (expClaim == null || expClaim is ! int ) {
234
+ print ('[invalidateToken] Failed: Missing or invalid "exp" claim.' );
235
+ throw const InvalidInputException (
236
+ 'Cannot invalidate token: Missing or invalid expiry (exp) claim.' ,
237
+ );
238
+ }
239
+ final expiryDateTime =
240
+ DateTime .fromMillisecondsSinceEpoch (expClaim * 1000 , isUtc: true );
241
+ print ('[invalidateToken] Extracted expiry: $expiryDateTime ' );
242
+
243
+ // 4. Add JTI to the blacklist
244
+ print ('[invalidateToken] Adding jti $jti to blacklist...' );
245
+ await _blacklistService.blacklist (jti, expiryDateTime);
246
+ print ('[invalidateToken] Token (jti: $jti ) successfully blacklisted.' );
247
+ } on JWTException catch (e, s) {
248
+ // Catch errors during the initial verification (e.g., bad signature)
249
+ print (
250
+ '[invalidateToken] CATCH JWTException: Invalid token format/signature. '
251
+ 'Reason: ${e .message }\n $s ' ,
252
+ );
253
+ // Treat as invalid input for invalidation purposes
254
+ throw InvalidInputException ('Invalid token format: ${e .message }' );
255
+ } on HtHttpException catch (e, s) {
256
+ // Catch errors from the blacklist service itself
257
+ print (
258
+ '[invalidateToken] CATCH HtHttpException: Error during blacklisting. '
259
+ 'Type: ${e .runtimeType }, Message: $e \n $s ' ,
260
+ );
261
+ // Re-throw blacklist service exceptions
262
+ rethrow ;
263
+ } catch (e, s) {
264
+ // Catch unexpected errors
265
+ print ('[invalidateToken] CATCH UNEXPECTED Exception: $e \n $s ' );
266
+ throw OperationFailedException (
267
+ 'Token invalidation failed unexpectedly: $e ' ,
268
+ );
269
+ }
270
+ }
172
271
}
0 commit comments