Skip to content

Commit 159be74

Browse files
committed
feat(auth): implement JWT auth token service
- Replaces simple token service - Uses dart_jsonwebtoken package - Adds JWT generation and validation - Improves security and scalability
1 parent 14ed27d commit 159be74

File tree

4 files changed

+140
-51
lines changed

4 files changed

+140
-51
lines changed

lib/src/services/auth_token_service.dart

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,52 +26,3 @@ abstract class AuthTokenService {
2626
// Potential future methods:
2727
// Future<void> invalidateToken(String token); // For token blacklisting
2828
}
29-
30-
/// A basic implementation of [AuthTokenService].
31-
///
32-
/// **Note:** This is a placeholder and **not secure** for production.
33-
/// It does not perform real cryptographic signing or validation.
34-
/// Replace with a proper JWT implementation (e.g., using `dart_jsonwebtoken`).
35-
class SimpleAuthTokenService implements AuthTokenService {
36-
/// {@macro simple_auth_token_service}
37-
const SimpleAuthTokenService({
38-
// In a real implementation, you'd inject a secret key here.
39-
String secretKey = 'very-secret-key-replace-me',
40-
}) : _secretKey = secretKey;
41-
42-
//
43-
// ignore: unused_field
44-
final String _secretKey;
45-
46-
// Placeholder for storing "valid" tokens in this insecure example.
47-
// A real implementation validates cryptographically.
48-
static final Map<String, User> _validTokens = {};
49-
50-
@override
51-
Future<String> generateToken(User user) async {
52-
// Insecure placeholder: Generate a simple token string.
53-
// A real implementation would create a JWT with claims and sign it.
54-
final token =
55-
'token_for_${user.id}_${DateTime.now().millisecondsSinceEpoch}';
56-
_validTokens[token] = user; // Store for simple validation
57-
print('Generated token (INSECURE): $token for user ${user.id}');
58-
await Future<void>.delayed(Duration.zero); // Simulate async
59-
return token;
60-
}
61-
62-
@override
63-
Future<User?> validateToken(String token) async {
64-
// Insecure placeholder: Check if the token exists in our map.
65-
// A real implementation would verify JWT signature, expiry, issuer, etc.
66-
print('Validating token (INSECURE): $token');
67-
final user = _validTokens[token];
68-
await Future<void>.delayed(Duration.zero); // Simulate async
69-
if (user != null) {
70-
print('Token valid for user ${user.id}');
71-
return user;
72-
} else {
73-
print('Token invalid');
74-
return null;
75-
}
76-
}
77-
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
2+
import 'package:ht_api/src/services/auth_token_service.dart';
3+
import 'package:ht_data_repository/ht_data_repository.dart';
4+
import 'package:ht_shared/ht_shared.dart';
5+
import 'package:uuid/uuid.dart';
6+
7+
/// {@template jwt_auth_token_service}
8+
/// An implementation of [AuthTokenService] using JSON Web Tokens (JWT).
9+
///
10+
/// Handles the creation (signing) and validation (verification) of JWTs
11+
/// for user authentication.
12+
/// {@endtemplate}
13+
class JwtAuthTokenService implements AuthTokenService {
14+
/// {@macro jwt_auth_token_service}
15+
///
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).
19+
const JwtAuthTokenService({
20+
required HtDataRepository<User> userRepository,
21+
required Uuid uuidGenerator,
22+
}) : _userRepository = userRepository,
23+
_uuid = uuidGenerator;
24+
25+
final HtDataRepository<User> _userRepository;
26+
final Uuid _uuid;
27+
28+
// --- Configuration ---
29+
30+
// WARNING: Hardcoding secrets is insecure. Use environment variables
31+
// or a proper secrets management solution in production.
32+
static const String _secretKey =
33+
'your-very-hardcoded-super-secret-key-replace-this-in-prod';
34+
35+
// Define token issuer and default expiry duration
36+
static const String _issuer = 'https://your-api-domain.com'; // Replace
37+
static const Duration _tokenExpiryDuration = Duration(hours: 1);
38+
39+
// --- Interface Implementation ---
40+
41+
@override
42+
Future<String> generateToken(User user) async {
43+
try {
44+
final now = DateTime.now();
45+
final expiry = now.add(_tokenExpiryDuration);
46+
47+
final jwt = JWT(
48+
{
49+
// Standard claims
50+
'sub': user.id, // Subject (user ID) - REQUIRED
51+
'exp': expiry.millisecondsSinceEpoch ~/ 1000, // Expiration Time
52+
'iat': now.millisecondsSinceEpoch ~/ 1000, // Issued At
53+
'iss': _issuer, // Issuer
54+
'jti': _uuid.v4(), // JWT ID (for potential blacklisting)
55+
56+
// Custom claims (optional, include what's useful)
57+
'email': user.email,
58+
'isAnonymous': user.isAnonymous,
59+
},
60+
issuer: _issuer,
61+
subject: user.id,
62+
jwtId: _uuid.v4(), // Re-setting jti here for clarity if needed
63+
);
64+
65+
// Sign the token using HMAC-SHA256
66+
final token = jwt.sign(
67+
SecretKey(_secretKey),
68+
algorithm: JWTAlgorithm.HS256,
69+
expiresIn: _tokenExpiryDuration, // Redundant but safe
70+
);
71+
72+
print('Generated JWT for user ${user.id}');
73+
return token;
74+
} catch (e) {
75+
print('Error generating JWT for user ${user.id}: $e');
76+
// Map to a standard exception
77+
throw OperationFailedException(
78+
'Failed to generate authentication token: ${e.toString()}',
79+
);
80+
}
81+
}
82+
83+
@override
84+
Future<User?> validateToken(String token) async {
85+
try {
86+
// Verify the token's signature and expiry
87+
final jwt = JWT.verify(token, SecretKey(_secretKey));
88+
89+
// Extract user ID from the subject claim
90+
final userId = jwt.payload['sub'] as String?;
91+
if (userId == null) {
92+
print('Token validation failed: Missing "sub" claim.');
93+
// Throw specific exception for malformed token
94+
throw const BadRequestException('Malformed token: Missing subject claim.');
95+
}
96+
97+
// Fetch the full user object from the repository
98+
// This ensures the user still exists and is valid
99+
final user = await _userRepository.read(userId);
100+
print('Token validated successfully for user ${user.id}');
101+
return user;
102+
} on JWTExpiredException {
103+
print('Token validation failed: Token expired.');
104+
// Throw specific exception for expired token
105+
throw const UnauthorizedException('Token expired.');
106+
} on JWTInvalidException catch (e) {
107+
print('Token validation failed: Invalid token. Reason: ${e.message}');
108+
// Throw specific exception for invalid token signature/format
109+
throw UnauthorizedException('Invalid token: ${e.message}');
110+
} on JWTException catch (e) { // Use JWTException as the general catch-all
111+
print('Token validation failed: JWT Exception. Reason: ${e.message}');
112+
// Treat other JWT exceptions as invalid tokens
113+
throw UnauthorizedException('Invalid token: ${e.message}');
114+
} on HtHttpException catch (e) {
115+
// Handle errors from the user repository (e.g., user not found)
116+
print('Token validation failed: Error fetching user $e');
117+
// Re-throw repository exceptions directly for the error handler
118+
rethrow;
119+
} catch (e) {
120+
// Catch unexpected errors during validation
121+
print('Unexpected error during token validation: $e');
122+
// Wrap unexpected errors in a standard exception type
123+
throw OperationFailedException(
124+
'Token validation failed unexpectedly: ${e.toString()}',
125+
);
126+
}
127+
}
128+
}

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ environment:
88

99
dependencies:
1010
dart_frog: ^1.1.0
11+
dart_jsonwebtoken: ^3.2.0
1112
ht_app_settings_client:
1213
git:
1314
url: https://github.com/headlines-toolkit/ht-app-settings-client.git

routes/_middleware.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ import 'package:dart_frog/dart_frog.dart';
99
import 'package:ht_api/src/middlewares/authentication_middleware.dart';
1010
import 'package:ht_api/src/middlewares/error_handler.dart';
1111
import 'package:ht_api/src/registry/model_registry.dart';
12+
import 'package:ht_api/src/middlewares/authentication_middleware.dart';
13+
import 'package:ht_api/src/middlewares/error_handler.dart';
14+
import 'package:ht_api/src/registry/model_registry.dart';
1215
import 'package:ht_api/src/services/auth_service.dart';
1316
import 'package:ht_api/src/services/auth_token_service.dart';
17+
// Import the new JWT service
18+
import 'package:ht_api/src/services/jwt_auth_token_service.dart';
1419
import 'package:ht_api/src/services/verification_code_storage_service.dart';
1520
import 'package:ht_app_settings_inmemory/ht_app_settings_inmemory.dart';
1621
import 'package:ht_app_settings_repository/ht_app_settings_repository.dart';
@@ -182,8 +187,12 @@ Handler middleware(Handler handler) {
182187
const emailRepository = HtEmailRepository(
183188
emailClient: HtEmailInMemoryClient(),
184189
);
185-
// Auth Services (using simple/in-memory implementations)
186-
const authTokenService = SimpleAuthTokenService();
190+
// Auth Services (using JWT and in-memory implementations)
191+
// Instantiate the new JWT service, passing its dependencies
192+
final authTokenService = JwtAuthTokenService(
193+
userRepository: userRepository,
194+
uuidGenerator: uuid,
195+
);
187196
final verificationCodeStorageService =
188197
InMemoryVerificationCodeStorageService();
189198
final authService = AuthService(

0 commit comments

Comments
 (0)