1+ import 'dart:convert' ; // For potential base64 decoding if needed
2+
13import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' ;
24import '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' ;
37import 'package:ht_data_repository/ht_data_repository.dart' ;
48import 'package:ht_shared/ht_shared.dart' ;
59import 'package:uuid/uuid.dart' ;
610
711/// {@template jwt_auth_token_service}
812/// An implementation of [AuthTokenService] using JSON Web Tokens (JWT).
913///
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 .
1216/// {@endtemplate}
1317class JwtAuthTokenService implements AuthTokenService {
1418 /// {@macro jwt_auth_token_service}
1519 ///
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).
1925 const JwtAuthTokenService ({
2026 required HtDataRepository <User > userRepository,
27+ required TokenBlacklistService blacklistService,
2128 required Uuid uuidGenerator,
2229 }) : _userRepository = userRepository,
30+ _blacklistService = blacklistService,
2331 _uuid = uuidGenerator;
2432
2533 final HtDataRepository <User > _userRepository;
34+ final TokenBlacklistService _blacklistService;
2635 final Uuid _uuid;
2736
2837 // --- Configuration ---
@@ -89,7 +98,30 @@ class JwtAuthTokenService implements AuthTokenService {
8998 final jwt = JWT .verify (token, SecretKey (_secretKey));
9099 print ('[validateToken] Token verified. Payload: ${jwt .payload }' );
91100
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')
93125 final subClaim = jwt.payload['sub' ];
94126 print (
95127 '[validateToken] Extracted "sub" claim: $subClaim '
@@ -104,12 +136,11 @@ class JwtAuthTokenService implements AuthTokenService {
104136 '[validateToken] "sub" claim successfully cast to String: $userId ' ,
105137 );
106138 } else if (subClaim != null ) {
139+ // Treat non-string sub as an error
107140 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 } ).' ,
110143 );
111- // Handle potential non-string types if necessary, or throw error
112- // For now, let's treat non-string sub as an error
113144 throw BadRequestException (
114145 'Malformed token: "sub" claim is not a String '
115146 '(Type: ${subClaim .runtimeType }).' ,
@@ -135,8 +166,8 @@ class JwtAuthTokenService implements AuthTokenService {
135166 return user;
136167 } on JWTExpiredException catch (e, s) {
137168 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 ;
140171 } on JWTInvalidException catch (e, s) {
141172 print (
142173 '[validateToken] CATCH JWTInvalidException: Invalid token. '
@@ -145,7 +176,7 @@ class JwtAuthTokenService implements AuthTokenService {
145176 // Throw specific exception for invalid token signature/format
146177 throw UnauthorizedException ('Invalid token: ${e .message }' );
147178 } 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
149180 print (
150181 '[validateToken] CATCH JWTException: General JWT error. '
151182 'Reason: ${e .message }\n $s ' ,
@@ -154,11 +185,12 @@ class JwtAuthTokenService implements AuthTokenService {
154185 throw UnauthorizedException ('Invalid token: ${e .message }' );
155186 } on HtHttpException catch (e, s) {
156187 // Handle errors from the user repository (e.g., user not found)
188+ // or blacklist check (if it threw HtHttpException)
157189 print (
158- '[validateToken] CATCH HtHttpException: Error fetching user . '
190+ '[validateToken] CATCH HtHttpException: Error during validation . '
159191 'Type: ${e .runtimeType }, Message: $e \n $s ' ,
160192 );
161- // Re-throw repository exceptions directly for the error handler
193+ // Re-throw repository/blacklist exceptions directly
162194 rethrow ;
163195 } catch (e, s) {
164196 // Catch unexpected errors during validation
@@ -169,4 +201,71 @@ class JwtAuthTokenService implements AuthTokenService {
169201 );
170202 }
171203 }
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+ }
172271}
0 commit comments