mirror of
https://github.com/foss42/apidash.git
synced 2025-08-18 01:18:21 +08:00
feat: enhance JWT support with private key handling and additional algorithms
This commit is contained in:
@ -21,6 +21,7 @@ class JwtAuthFields extends StatefulWidget {
|
||||
|
||||
class _JwtAuthFieldsState extends State<JwtAuthFields> {
|
||||
late TextEditingController _secretController;
|
||||
late TextEditingController _privateKeyController;
|
||||
late TextEditingController _payloadController;
|
||||
late String _addTokenTo;
|
||||
late String _algorithm;
|
||||
@ -31,6 +32,7 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
|
||||
super.initState();
|
||||
final jwt = widget.authData?.jwt;
|
||||
_secretController = TextEditingController(text: jwt?.secret ?? '');
|
||||
_privateKeyController = TextEditingController(text: jwt?.privateKey ?? '');
|
||||
_payloadController = TextEditingController(text: jwt?.payload ?? '');
|
||||
_addTokenTo = jwt?.addTokenTo ?? 'header';
|
||||
_algorithm = jwt?.algorithm ?? 'HS256';
|
||||
@ -83,6 +85,17 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
|
||||
('HS256', 'HS256'),
|
||||
('HS384', 'HS384'),
|
||||
('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",
|
||||
isOutlined: true,
|
||||
@ -96,33 +109,78 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AuthTextField(
|
||||
readOnly: widget.readOnly,
|
||||
controller: _secretController,
|
||||
isObscureText: true,
|
||||
hintText: "Secret key",
|
||||
onChanged: (value) => _updateJwtAuth(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CheckboxListTile(
|
||||
title: Text(
|
||||
"Secret is Base64 encoded",
|
||||
if (_algorithm.startsWith('HS')) ...[
|
||||
AuthTextField(
|
||||
readOnly: widget.readOnly,
|
||||
controller: _secretController,
|
||||
isObscureText: true,
|
||||
hintText: "Secret key",
|
||||
onChanged: (value) => _updateJwtAuth(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CheckboxListTile(
|
||||
title: Text(
|
||||
"Secret is Base64 encoded",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
value: _isSecretBase64Encoded,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
_isSecretBase64Encoded = value ?? false;
|
||||
});
|
||||
|
||||
_updateJwtAuth();
|
||||
},
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
"Private Key (PEM Format)",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
value: _isSecretBase64Encoded,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
_isSecretBase64Encoded = value ?? false;
|
||||
});
|
||||
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(),
|
||||
),
|
||||
],
|
||||
|
||||
_updateJwtAuth();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Payload (JSON format)",
|
||||
@ -284,6 +342,7 @@ class _JwtAuthFieldsState extends State<JwtAuthFields> {
|
||||
type: APIAuthType.jwt,
|
||||
jwt: AuthJwtModel(
|
||||
secret: _secretController.text.trim(),
|
||||
privateKey: _privateKeyController.text.trim(),
|
||||
payload: _payloadController.text.trim(),
|
||||
addTokenTo: _addTokenTo,
|
||||
algorithm: _algorithm,
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user