feat: enhance JWT support with private key handling and additional algorithms

This commit is contained in:
Udhay-Adithya
2025-07-06 12:19:27 +05:30
parent 92af4fba77
commit d5ca13b356
8 changed files with 388 additions and 85 deletions

View File

@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
adaptive_number:
dependency: transitive
description:
name: adaptive_number
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
async:
dependency: transitive
description:
@@ -72,6 +80,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dart_jsonwebtoken:
dependency: transitive
description:
name: dart_jsonwebtoken
sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
ed25519_edwards:
dependency: transitive
description:
name: ed25519_edwards
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
fake_async:
dependency: transitive
description:
@@ -80,6 +104,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@@ -210,6 +242,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.0"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
seed:
dependency: "direct overridden"
description:

View File

@@ -7,6 +7,7 @@ part 'auth_jwt_model.g.dart';
class AuthJwtModel with _$AuthJwtModel {
const factory AuthJwtModel({
required String secret,
String? privateKey,
required String payload,
required String addTokenTo,
required String algorithm,

View File

@@ -22,6 +22,7 @@ AuthJwtModel _$AuthJwtModelFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$AuthJwtModel {
String get secret => throw _privateConstructorUsedError;
String? get privateKey => throw _privateConstructorUsedError;
String get payload => throw _privateConstructorUsedError;
String get addTokenTo => throw _privateConstructorUsedError;
String get algorithm => throw _privateConstructorUsedError;
@@ -49,6 +50,7 @@ abstract class $AuthJwtModelCopyWith<$Res> {
@useResult
$Res call({
String secret,
String? privateKey,
String payload,
String addTokenTo,
String algorithm,
@@ -75,6 +77,7 @@ class _$AuthJwtModelCopyWithImpl<$Res, $Val extends AuthJwtModel>
@override
$Res call({
Object? secret = null,
Object? privateKey = freezed,
Object? payload = null,
Object? addTokenTo = null,
Object? algorithm = null,
@@ -89,6 +92,10 @@ class _$AuthJwtModelCopyWithImpl<$Res, $Val extends AuthJwtModel>
? _value.secret
: secret // ignore: cast_nullable_to_non_nullable
as String,
privateKey: freezed == privateKey
? _value.privateKey
: privateKey // ignore: cast_nullable_to_non_nullable
as String?,
payload: null == payload
? _value.payload
: payload // ignore: cast_nullable_to_non_nullable
@@ -134,6 +141,7 @@ abstract class _$$AuthJwtModelImplCopyWith<$Res>
@useResult
$Res call({
String secret,
String? privateKey,
String payload,
String addTokenTo,
String algorithm,
@@ -159,6 +167,7 @@ class __$$AuthJwtModelImplCopyWithImpl<$Res>
@override
$Res call({
Object? secret = null,
Object? privateKey = freezed,
Object? payload = null,
Object? addTokenTo = null,
Object? algorithm = null,
@@ -173,6 +182,10 @@ class __$$AuthJwtModelImplCopyWithImpl<$Res>
? _value.secret
: secret // ignore: cast_nullable_to_non_nullable
as String,
privateKey: freezed == privateKey
? _value.privateKey
: privateKey // ignore: cast_nullable_to_non_nullable
as String?,
payload: null == payload
? _value.payload
: payload // ignore: cast_nullable_to_non_nullable
@@ -211,6 +224,7 @@ class __$$AuthJwtModelImplCopyWithImpl<$Res>
class _$AuthJwtModelImpl implements _AuthJwtModel {
const _$AuthJwtModelImpl({
required this.secret,
this.privateKey,
required this.payload,
required this.addTokenTo,
required this.algorithm,
@@ -226,6 +240,8 @@ class _$AuthJwtModelImpl implements _AuthJwtModel {
@override
final String secret;
@override
final String? privateKey;
@override
final String payload;
@override
final String addTokenTo;
@@ -242,7 +258,7 @@ class _$AuthJwtModelImpl implements _AuthJwtModel {
@override
String toString() {
return 'AuthJwtModel(secret: $secret, payload: $payload, addTokenTo: $addTokenTo, algorithm: $algorithm, isSecretBase64Encoded: $isSecretBase64Encoded, headerPrefix: $headerPrefix, queryParamKey: $queryParamKey, header: $header)';
return 'AuthJwtModel(secret: $secret, privateKey: $privateKey, payload: $payload, addTokenTo: $addTokenTo, algorithm: $algorithm, isSecretBase64Encoded: $isSecretBase64Encoded, headerPrefix: $headerPrefix, queryParamKey: $queryParamKey, header: $header)';
}
@override
@@ -251,6 +267,8 @@ class _$AuthJwtModelImpl implements _AuthJwtModel {
(other.runtimeType == runtimeType &&
other is _$AuthJwtModelImpl &&
(identical(other.secret, secret) || other.secret == secret) &&
(identical(other.privateKey, privateKey) ||
other.privateKey == privateKey) &&
(identical(other.payload, payload) || other.payload == payload) &&
(identical(other.addTokenTo, addTokenTo) ||
other.addTokenTo == addTokenTo) &&
@@ -270,6 +288,7 @@ class _$AuthJwtModelImpl implements _AuthJwtModel {
int get hashCode => Object.hash(
runtimeType,
secret,
privateKey,
payload,
addTokenTo,
algorithm,
@@ -296,6 +315,7 @@ class _$AuthJwtModelImpl implements _AuthJwtModel {
abstract class _AuthJwtModel implements AuthJwtModel {
const factory _AuthJwtModel({
required final String secret,
final String? privateKey,
required final String payload,
required final String addTokenTo,
required final String algorithm,
@@ -311,6 +331,8 @@ abstract class _AuthJwtModel implements AuthJwtModel {
@override
String get secret;
@override
String? get privateKey;
@override
String get payload;
@override
String get addTokenTo;

View File

@@ -9,6 +9,7 @@ part of 'auth_jwt_model.dart';
_$AuthJwtModelImpl _$$AuthJwtModelImplFromJson(Map<String, dynamic> json) =>
_$AuthJwtModelImpl(
secret: json['secret'] as String,
privateKey: json['privateKey'] as String?,
payload: json['payload'] as String,
addTokenTo: json['addTokenTo'] as String,
algorithm: json['algorithm'] as String,
@@ -21,6 +22,7 @@ _$AuthJwtModelImpl _$$AuthJwtModelImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$AuthJwtModelImplToJson(_$AuthJwtModelImpl instance) =>
<String, dynamic>{
'secret': instance.secret,
'privateKey': instance.privateKey,
'payload': instance.payload,
'addTokenTo': instance.addTokenTo,
'algorithm': instance.algorithm,

View File

@@ -1,94 +1,95 @@
import 'dart:convert';
import 'dart:typed_data';
import 'dart:developer';
import 'package:better_networking/models/auth/auth_jwt_model.dart';
import 'package:crypto/crypto.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
String generateJWT(AuthJwtModel jwtAuth) {
try {
Map<String, dynamic> header;
// Parse header if provided
Map<String, dynamic> headerMap = {};
if (jwtAuth.header.isNotEmpty) {
try {
header = json.decode(jwtAuth.header) as Map<String, dynamic>;
headerMap = json.decode(jwtAuth.header) as Map<String, dynamic>;
} catch (e) {
header = {};
// If header parsing fails, use empty header
headerMap = {};
}
} else {
header = {};
}
header['typ'] = header['typ'] ?? 'JWT';
header['alg'] = jwtAuth.algorithm;
Map<String, dynamic> payload;
// Parse payload if provided
Map<String, dynamic> payloadMap = {};
if (jwtAuth.payload.isNotEmpty) {
try {
payload = json.decode(jwtAuth.payload) as Map<String, dynamic>;
payloadMap = json.decode(jwtAuth.payload) as Map<String, dynamic>;
} catch (e) {
payload = {};
// If payload parsing fails, use empty payload
payloadMap = {};
}
} else {
payload = {};
}
if (!payload.containsKey('iat')) {
payload['iat'] = DateTime.now().millisecondsSinceEpoch ~/ 1000;
}
// Encode header and payload
final encodedHeader = _base64UrlEncode(utf8.encode(json.encode(header)));
final encodedPayload = _base64UrlEncode(utf8.encode(json.encode(payload)));
// Add issued at time if not present
if (!payloadMap.containsKey('iat')) {
payloadMap['iat'] = DateTime.now().millisecondsSinceEpoch ~/ 1000;
}
final jwt = JWT(payloadMap, header: headerMap);
// Create signature
final signature = _createSignature(
'$encodedHeader.$encodedPayload',
final key = _createKey(
jwtAuth.secret,
jwtAuth.algorithm,
jwtAuth.isSecretBase64Encoded,
jwtAuth.privateKey,
);
final token = jwt.sign(
key,
algorithm: JWTAlgorithm.fromName(jwtAuth.algorithm),
);
return '$encodedHeader.$encodedPayload.$signature';
return token;
} catch (e) {
throw Exception('Failed to generate JWT: $e');
log(e.toString());
throw Exception('Failed to generate JSON Wweb Token: $e');
}
}
String _createSignature(
String data, String secret, String algorithm, bool isSecretBase64Encoded) {
try {
Uint8List secretBytes;
JWTKey _createKey(
String secret,
String algorithm,
bool isSecretBase64Encoded,
String? privateKey,
) {
if (algorithm.startsWith('HS')) {
if (isSecretBase64Encoded) {
secretBytes = base64.decode(secret);
final decodedSecret = base64.decode(secret);
return SecretKey(String.fromCharCodes(decodedSecret));
} else {
secretBytes = utf8.encode(secret);
return SecretKey(secret);
}
final dataBytes = utf8.encode(data);
switch (algorithm) {
case 'HS256':
final hmac = Hmac(sha256, secretBytes);
final digest = hmac.convert(dataBytes);
return _base64UrlEncode(digest.bytes);
case 'HS384':
final hmac = Hmac(sha384, secretBytes);
final digest = hmac.convert(dataBytes);
return _base64UrlEncode(digest.bytes);
case 'HS512':
final hmac = Hmac(sha512, secretBytes);
final digest = hmac.convert(dataBytes);
return _base64UrlEncode(digest.bytes);
default:
// Default to HS256
final hmac = Hmac(sha256, secretBytes);
final digest = hmac.convert(dataBytes);
return _base64UrlEncode(digest.bytes);
}
} catch (e) {
// Return placeholder signature if creation fails
return _base64UrlEncode(utf8.encode('signature_generation_failed'));
}
}
if (algorithm.startsWith('RS') || algorithm.startsWith('PS')) {
if (privateKey == null) {
throw Exception(
'Failed to generate JSON Wweb Token: Private Key not Found',
);
}
return RSAPrivateKey(privateKey);
}
if (algorithm.startsWith('ES')) {
if (privateKey == null) {
throw Exception(
'Failed to generate JSON Wweb Token: Private Key not Found',
);
}
return ECPrivateKey(privateKey);
}
String _base64UrlEncode(List<int> bytes) {
return base64Url.encode(bytes).replaceAll('=', '');
if (algorithm == 'EdDSA') {
if (privateKey == null) {
throw Exception(
'Failed to generate JSON Wweb Token: Private Key not Found',
);
}
return EdDSAPrivateKey.fromPEM(privateKey);
}
return SecretKey(secret, isBase64Encoded: isSecretBase64Encoded);
}

View File

@@ -26,7 +26,7 @@ dependencies:
seed: ^0.0.3
xml: ^6.3.0
crypto: ^3.0.6
convert: ^3.1.2
dart_jsonwebtoken: ^3.2.0
dev_dependencies:
flutter_test:

View File

@@ -0,0 +1,178 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:better_networking/models/auth/auth_jwt_model.dart';
import 'package:better_networking/utils/auth/jwt_auth_utils.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
void main() {
group('JWT Auth Utils Tests', () {
test('should generate JWT with HS256 algorithm', () {
const jwtAuth = AuthJwtModel(
secret: 'test_secret',
payload: '{"user_id": 123, "username": "testuser"}',
addTokenTo: 'header',
algorithm: 'HS256',
isSecretBase64Encoded: false,
headerPrefix: 'Bearer',
queryParamKey: 'token',
header: '{"typ": "JWT"}',
);
final token = generateJWT(jwtAuth);
expect(token, isNotEmpty);
expect(token.split('.').length, equals(3)); // JWT has 3 parts
// Verify the token can be decoded
final decoded = JWT.decode(token);
expect(decoded.payload['user_id'], equals(123));
expect(decoded.payload['username'], equals('testuser'));
});
test('should generate JWT with HS384 algorithm', () {
const jwtAuth = AuthJwtModel(
secret: 'test_secret_384',
payload: '{"role": "admin"}',
addTokenTo: 'header',
algorithm: 'HS384',
isSecretBase64Encoded: false,
headerPrefix: 'Bearer',
queryParamKey: 'token',
header: '',
);
final token = generateJWT(jwtAuth);
expect(token, isNotEmpty);
expect(token.split('.').length, equals(3));
// Verify the token can be decoded
final decoded = JWT.decode(token);
expect(decoded.payload['role'], equals('admin'));
});
test('should generate JWT with HS512 algorithm', () {
const jwtAuth = AuthJwtModel(
secret: 'test_secret_512',
payload: '{"exp": 1234567890}',
addTokenTo: 'header',
algorithm: 'HS512',
isSecretBase64Encoded: false,
headerPrefix: 'Bearer',
queryParamKey: 'token',
header: '',
);
final token = generateJWT(jwtAuth);
expect(token, isNotEmpty);
expect(token.split('.').length, equals(3));
// Verify the token can be decoded
final decoded = JWT.decode(token);
expect(decoded.payload['exp'], equals(1234567890));
});
test('should generate JWT with base64 encoded secret', () {
const secretBase64 = 'dGVzdF9zZWNyZXQ='; // base64 encoded "test_secret"
const jwtAuth = AuthJwtModel(
secret: secretBase64,
payload: '{"test": "value"}',
addTokenTo: 'header',
algorithm: 'HS256',
isSecretBase64Encoded: true,
headerPrefix: 'Bearer',
queryParamKey: 'token',
header: '',
);
final token = generateJWT(jwtAuth);
expect(token, isNotEmpty);
expect(token.split('.').length, equals(3));
// Verify the token can be decoded
final decoded = JWT.decode(token);
expect(decoded.payload['test'], equals('value'));
});
test('should handle empty payload', () {
const jwtAuth = AuthJwtModel(
secret: 'test_secret',
payload: '',
addTokenTo: 'header',
algorithm: 'HS256',
isSecretBase64Encoded: false,
headerPrefix: 'Bearer',
queryParamKey: 'token',
header: '',
);
final token = generateJWT(jwtAuth);
expect(token, isNotEmpty);
expect(token.split('.').length, equals(3));
// Verify the token can be decoded and has iat
final decoded = JWT.decode(token);
expect(decoded.payload['iat'], isNotNull);
});
test('should handle invalid JSON payload gracefully', () {
const jwtAuth = AuthJwtModel(
secret: 'test_secret',
payload: 'invalid json',
addTokenTo: 'header',
algorithm: 'HS256',
isSecretBase64Encoded: false,
headerPrefix: 'Bearer',
queryParamKey: 'token',
header: '',
);
final token = generateJWT(jwtAuth);
expect(token, isNotEmpty);
expect(token.split('.').length, equals(3));
// Should have at least iat in payload
final decoded = JWT.decode(token);
expect(decoded.payload['iat'], isNotNull);
});
test('should verify generated JWT with correct secret', () {
const secret = 'verification_secret';
const jwtAuth = AuthJwtModel(
secret: secret,
payload: '{"user": "test"}',
addTokenTo: 'header',
algorithm: 'HS256',
isSecretBase64Encoded: false,
headerPrefix: 'Bearer',
queryParamKey: 'token',
header: '',
);
final token = generateJWT(jwtAuth);
// Verify with correct secret
expect(() => JWT.verify(token, SecretKey(secret)), returnsNormally);
});
test('should fail verification with wrong secret', () {
const secret = 'correct_secret';
const wrongSecret = 'wrong_secret';
const jwtAuth = AuthJwtModel(
secret: secret,
payload: '{"user": "test"}',
addTokenTo: 'header',
algorithm: 'HS256',
isSecretBase64Encoded: false,
headerPrefix: 'Bearer',
queryParamKey: 'token',
header: '',
);
final token = generateJWT(jwtAuth);
// Verify with wrong secret should throw
expect(
() => JWT.verify(token, SecretKey(wrongSecret)),
throwsA(isA<JWTException>()),
);
});
});
}