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

@ -21,6 +21,7 @@ class JwtAuthFields extends StatefulWidget {
class _JwtAuthFieldsState extends State<JwtAuthFields> { class _JwtAuthFieldsState extends State<JwtAuthFields> {
late TextEditingController _secretController; late TextEditingController _secretController;
late TextEditingController _privateKeyController;
late TextEditingController _payloadController; late TextEditingController _payloadController;
late String _addTokenTo; late String _addTokenTo;
late String _algorithm; late String _algorithm;
@ -31,6 +32,7 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
super.initState(); super.initState();
final jwt = widget.authData?.jwt; final jwt = widget.authData?.jwt;
_secretController = TextEditingController(text: jwt?.secret ?? ''); _secretController = TextEditingController(text: jwt?.secret ?? '');
_privateKeyController = TextEditingController(text: jwt?.privateKey ?? '');
_payloadController = TextEditingController(text: jwt?.payload ?? ''); _payloadController = TextEditingController(text: jwt?.payload ?? '');
_addTokenTo = jwt?.addTokenTo ?? 'header'; _addTokenTo = jwt?.addTokenTo ?? 'header';
_algorithm = jwt?.algorithm ?? 'HS256'; _algorithm = jwt?.algorithm ?? 'HS256';
@ -83,6 +85,17 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
('HS256', 'HS256'), ('HS256', 'HS256'),
('HS384', 'HS384'), ('HS384', 'HS384'),
('HS512', 'HS512'), ('HS512', 'HS512'),
('RS256', 'RS256'),
('RS384', 'RS384'),
('RS512', 'RS512'),
('PS256', 'PS256'),
('PS384', 'PS384'),
('PS512', 'PS512'),
('ES256', 'ES256'),
('ES256K', 'ES256K'),
('ES384', 'ES384'),
('ES512', 'ES512'),
('EdDSA', 'EdDSA'),
], ],
tooltip: "Select JWT algorithm", tooltip: "Select JWT algorithm",
isOutlined: true, isOutlined: true,
@ -96,6 +109,7 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (_algorithm.startsWith('HS')) ...[
AuthTextField( AuthTextField(
readOnly: widget.readOnly, readOnly: widget.readOnly,
controller: _secretController, controller: _secretController,
@ -123,6 +137,50 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
_updateJwtAuth(); _updateJwtAuth();
}, },
), ),
] else ...[
Text(
"Private Key (PEM Format)",
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
SizedBox(height: 4),
TextField(
readOnly: widget.readOnly,
controller: _privateKeyController,
maxLines: 5,
decoration: InputDecoration(
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerLowest,
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width - 100,
),
contentPadding: const EdgeInsets.all(18),
hintText: '''
-----BEGIN RSA PRIVATE KEY-----
Private Key in PKCS#8 PEM Format
-----END RSA PRIVATE KEY-----
''',
hintStyle: Theme.of(context).textTheme.bodyMedium,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
),
),
onChanged: (value) => _updateJwtAuth(),
),
],
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
"Payload (JSON format)", "Payload (JSON format)",
@ -284,6 +342,7 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
type: APIAuthType.jwt, type: APIAuthType.jwt,
jwt: AuthJwtModel( jwt: AuthJwtModel(
secret: _secretController.text.trim(), secret: _secretController.text.trim(),
privateKey: _privateKeyController.text.trim(),
payload: _payloadController.text.trim(), payload: _payloadController.text.trim(),
addTokenTo: _addTokenTo, addTokenTo: _addTokenTo,
algorithm: _algorithm, algorithm: _algorithm,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ dependencies:
seed: ^0.0.3 seed: ^0.0.3
xml: ^6.3.0 xml: ^6.3.0
crypto: ^3.0.6 crypto: ^3.0.6
convert: ^3.1.2 dart_jsonwebtoken: ^3.2.0
dev_dependencies: dev_dependencies:
flutter_test: 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>()),
);
});
});
}