refactor(jwt): upgrade analysis_options (#2715)

This commit is contained in:
Felix Angelov
2024-12-19 11:50:58 -06:00
committed by GitHub
parent 643ab96140
commit c5db92fa1a
13 changed files with 127 additions and 41 deletions
+3 -1
View File
@@ -1 +1,3 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
include: package:very_good_analysis/analysis_options.7.0.0.yaml
analyzer:
exclude: ["example/**"]
+1
View File
@@ -8,6 +8,7 @@ targets:
- require_trailing_commas
- cast_nullable_to_non_nullable
- lines_longer_than_80_chars
- document_ignores
json_serializable:
options:
field_rename: snake
-1
View File
@@ -1,4 +1,3 @@
// ignore_for_file: avoid_print
import 'package:jwt/jwt.dart' as jwt;
Future<void> main() async {
+18 -17
View File
@@ -90,8 +90,13 @@ Future<Jwt> verify(
// By using this keystore's key IDs to validate the header above, we've
// guaranteed that there is a public key for this key ID.
final publicKey = publicKeys.getPublicKey(jwt.header.kid)!;
final bool isValid;
try {
isValid = _verifySignature(encodedJwt, publicKey);
} on Exception {
throw const JwtVerificationFailure('JWT signature is malformed.');
}
final isValid = _verifySignature(encodedJwt, publicKey);
if (!isValid) {
throw const JwtVerificationFailure('Invalid signature.');
}
@@ -165,22 +170,18 @@ bool _verifySignature(String jwt, String publicKey) {
if (pair.public is! rsa.RSAPublicKey) return false;
final public = pair.public;
try {
final signer = Signer('SHA-256/RSA');
final key = RSAPublicKey(
public!.modulus,
BigInt.from(public.publicExponent),
);
final param = ParametersWithRandom(
PublicKeyParameter<RSAPublicKey>(key),
SecureRandom('AES/CTR/PRNG'),
);
signer.init(false, param);
final rsaSignature = RSASignature(Uint8List.fromList(sign));
return signer.verifySignature(Uint8List.fromList(body), rsaSignature);
} catch (_) {
return false;
}
final signer = Signer('SHA-256/RSA');
final key = RSAPublicKey(
public!.modulus,
BigInt.from(public.publicExponent),
);
final param = ParametersWithRandom(
PublicKeyParameter<RSAPublicKey>(key),
SecureRandom('AES/CTR/PRNG'),
);
signer.init(false, param);
final rsaSignature = RSASignature(Uint8List.fromList(sign));
return signer.verifySignature(Uint8List.fromList(body), rsaSignature);
}
/// Visible for testing only
+1 -1
View File
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, document_ignores
part of 'jwk.dart';
+2 -2
View File
@@ -24,14 +24,14 @@ class Jwt {
final JwtHeader header;
try {
header = JwtHeader.fromJson(decodeJwtPart(parts[0]));
} catch (_) {
} on Exception {
throw const FormatException('JWT header is malformed.');
}
final JwtPayload payload;
try {
payload = JwtPayload.fromJson(decodeJwtPart(parts[1]));
} catch (_) {
} on Exception {
throw const FormatException('JWT payload is malformed.');
}
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, document_ignores
part of 'jwt_header.dart';
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, document_ignores
part of 'jwt_payload.dart';
@@ -13,12 +13,12 @@ JwtPayload _$JwtPayloadFromJson(Map<String, dynamic> json) => $checkedCreate(
json,
($checkedConvert) {
final val = JwtPayload(
exp: $checkedConvert('exp', (v) => v as int),
iat: $checkedConvert('iat', (v) => v as int),
exp: $checkedConvert('exp', (v) => (v as num).toInt()),
iat: $checkedConvert('iat', (v) => (v as num).toInt()),
aud: $checkedConvert('aud', (v) => v as String),
iss: $checkedConvert('iss', (v) => v as String),
sub: $checkedConvert('sub', (v) => v as String),
authTime: $checkedConvert('auth_time', (v) => v as int?),
authTime: $checkedConvert('auth_time', (v) => (v as num?)?.toInt()),
);
return val;
},
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, document_ignores
part of 'jwk_key_store.dart';
@@ -10,8 +10,14 @@ class KeyValueKeyStore extends PublicKeyStore {
const KeyValueKeyStore({required this.keys});
/// Decodes a JSON object into a [KeyValueKeyStore].
factory KeyValueKeyStore.fromJson(Map<String, dynamic> json) =>
KeyValueKeyStore(keys: json.map((k, v) => MapEntry(k, v as String)));
factory KeyValueKeyStore.fromJson(Map<String, dynamic> json) {
return KeyValueKeyStore(
keys: json.map((k, v) {
if (v is! String) throw const FormatException('value is not a string.');
return MapEntry(k, v);
}),
);
}
/// Map of all public key id/value pairs.
final Map<String, String> keys;
@@ -15,11 +15,15 @@ abstract class PublicKeyStore {
if (json.containsKey('keys')) {
try {
return JwkKeyStore.fromJson(json);
} catch (_) {}
} on Exception {
// Swallow deserialization exceptions and return null.
}
} else {
try {
return KeyValueKeyStore.fromJson(json);
} catch (_) {}
} on Exception {
// Swallow deserialization exceptions and return null.
}
}
return null;
+2 -2
View File
@@ -10,7 +10,7 @@ dependencies:
clock: ^1.1.0
collection: ^1.18.0
http: ^1.0.0
json_annotation: ^4.4.0
json_annotation: ^4.9.0
meta: ^1.7.0
pointycastle: ^3.5.0
rsa_pkcs: ^2.0.0
@@ -21,4 +21,4 @@ dev_dependencies:
json_serializable: ^6.1.4
path: ^1.9.0
test: ^1.19.2
very_good_analysis: ^6.0.0
very_good_analysis: ^7.0.0
+80 -7
View File
@@ -1,4 +1,3 @@
// ignore_for_file: prefer_const_constructors
import 'dart:io';
import 'package:clock/clock.dart';
@@ -10,10 +9,14 @@ import 'package:test/test.dart';
void main() {
const token = // cspell: disable-next-line
'''eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxMGM5MGJhNGMzNjYzNTE2ZTA3MDdkMGU5YTg5NDgxMDYyODUxNTgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwiYXV0aF90aW1lIjoxNjQzNjg0MjY2LCJ1c2VyX2lkIjoiRzR1MzdXdk90dmVWR0pRb1pCWGpxcHVWazZWMiIsInN1YiI6Ikc0dTM3V3ZPdHZlVkdKUW9aQlhqcXB1Vms2VjIiLCJpYXQiOjE2NDM2ODQyNjYsImV4cCI6MTY0MzY4Nzg2NiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3RAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.bUWnX_XmR1d9EmeFeYSsK_CHU1u9NPIHgyaQueZ6urYOtxvuL_QodjPl0c9CBJwctwPnxVyRmkeNCw0oF9xBgph0NApLL4FIG6vpDPZfW9txZBYr8xIvaqvmD0diACENAQdjRT2XmyEdQ2-U7SsTonybHmLoU9FMQTjAgw4NCALQvExfB6rtQ9GDsOBt1xoBkB3Vo7a5OmugZ1aHXF69b8As6137-Dggf5qx5R3oLRFovICMMesQziE3vGi-WKcbQxSeiD-9a6ShPAhk41XiyjFGDEOtUCQo63uwQnMw3g0KVtC6bzIyFq-E91vhxumxXzxPYC-kg7iUYiSZy7Y-Aw''';
const tokenInvalidPayload = // cspell: disable-next-line
'''eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxMGM5MGJhNGMzNjYzNTE2ZTA3MDdkMGU5YTg5NDgxMDYyODUxNTgiLCJ0eXAiOiJKV1QifQ.invalid.bUWnX_XmR1d9EmeFeYSsK_CHU1u9NPIHgyaQueZ6urYOtxvuL_QodjPl0c9CBJwctwPnxVyRmkeNCw0oF9xBgph0NApLL4FIG6vpDPZfW9txZBYr8xIvaqvmD0diACENAQdjRT2XmyEdQ2-U7SsTonybHmLoU9FMQTjAgw4NCALQvExfB6rtQ9GDsOBt1xoBkB3Vo7a5OmugZ1aHXF69b8As6137-Dggf5qx5R3oLRFovICMMesQziE3vGi-WKcbQxSeiD-9a6ShPAhk41XiyjFGDEOtUCQo63uwQnMw3g0KVtC6bzIyFq-E91vhxumxXzxPYC-kg7iUYiSZy7Y-Aw''';
const tokenNoAuthTime = // cspell: disable-next-line
'''eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxMGM5MGJhNGMzNjYzNTE2ZTA3MDdkMGU5YTg5NDgxMDYyODUxNTgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwidXNlcl9pZCI6Ikc0dTM3V3ZPdHZlVkdKUW9aQlhqcXB1Vms2VjIiLCJzdWIiOiJHNHUzN1d2T3R2ZVZHSlFvWkJYanFwdVZrNlYyIiwiaWF0IjoxNjQzNjg0MjY2LCJleHAiOjE2NDM2ODc4NjYsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJ0ZXN0QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.ZWCadE43mUk43cPQdNCCi4WhDgB4ZsDT9rhPGQq_1uFPhzkVrCSRcjUhwkzH11VLap_MVurNvI_pGWbu9Z4CRPvGFzXPpuNveWy2qFPEa4jcM-R40vsbrP30vNnrp4PrmqgLar0vWs6FZ2g9fbjU8L1LaU5ik31OKSXufTIKn_hPHhyIC33tYTpWzG3Abq3H9EELHUXKW9nEcN8YYnOHAZ3A6ymb3DyBguhf2O-XAIlrn1WoxRRqlukFGSmprk7heonbVUTzoc3sIDZcC-Cj1U9wTee1NmqmU7v3SvpBRGnuXz-5rzSRHblyVxn_EEfCYwjsDUwetYpyFcCs5dqPlQ''';
const tokenWithNoMatchingKid = // cspell: disable-next-line
'''eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwidXNlcl9pZCI6Ikc0dTM3V3ZPdHZlVkdKUW9aQlhqcXB1Vms2VjIiLCJzdWIiOiJHNHUzN1d2T3R2ZVZHSlFvWkJYanFwdVZrNlYyIiwiaWF0IjoxNjQzNjg0MjY2LCJleHAiOjE2NDM2ODc4NjYsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJ0ZXN0QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.RBAwg-Ttf36aDkVR97Rd50aMp-0yzk8do_4AUPFi9nEhKu0Ye8ox_9hdTBttJYxoOq0NbH2zz0JHSnSHqoTvCleVhoGqg8YghzH0NiqncPfDzi-IcRfy2K8CrOoXuqXaj3YWrbrzNWAYV46eFmvI2TmPO55AyFOjhvLpW-uf96ceOPjueZm8o5K2DZym86BAhSdShknONV2O7b2vW34TXf3UJdISs5p9z6Si4JOWGjbVPY45CO16ODdDxyUGUM2-IVQloB7bg0nMqQbIjKDoO5g9d1nguR2Z-YBiv0BX1MDWDlrBsMAzzRVaFlf6YQS1LKxu2tnMdCrBnK_Wvqt5sQ''';
const tokenInvalidSignature = // cspell: disable-next-line
'''eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxMGM5MGJhNGMzNjYzNTE2ZTA3MDdkMGU5YTg5NDgxMDYyODUxNTgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwiYXV0aF90aW1lIjoxNjQzNjg0MjY2LCJ1c2VyX2lkIjoiRzR1MzdXdk90dmVWR0pRb1pCWGpxcHVWazZWMiIsInN1YiI6Ikc0dTM3V3ZPdHZlVkdKUW9aQlhqcXB1Vms2VjIiLCJpYXQiOjE2NDM2ODQyNjYsImV4cCI6MTY0MzY4Nzg2NiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3RAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.invalid-signature''';
const keyValuePublicKeysUrl =
'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
const jwkPublicKeysUrl =
@@ -24,7 +27,7 @@ void main() {
File(p.join('test', 'fixtures', 'key_value_key_store.json'))
.readAsStringSync();
final expiresAt = DateTime.fromMillisecondsSinceEpoch(1643687866 * 1000);
final validTime = expiresAt.subtract(Duration(minutes: 15));
final validTime = expiresAt.subtract(const Duration(minutes: 15));
late String keyStoreResponseBody;
@@ -85,6 +88,45 @@ void main() {
);
});
test('throws a JwtVerificationFailure if payload is not valid', () async {
await expectLater(
() => verify(
tokenInvalidPayload,
audience: {audience},
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
),
throwsA(
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'JWT payload is malformed.',
),
),
);
});
test('throws a JwtVerificationFailure if signature is not valid',
() async {
await withClock(Clock.fixed(validTime), () async {
await expectLater(
() => verify(
tokenInvalidSignature,
audience: {audience},
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
),
throwsA(
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'JWT signature is malformed.',
),
),
);
});
});
test('throws exception if jwt has no matching public key id', () async {
await withClock(Clock.fixed(validTime), () async {
await expectLater(
@@ -105,8 +147,9 @@ void main() {
});
});
test('throws exception if invalid keys are provided by the publicKeysUrl',
() async {
test(
'throws exception if invalid keys are provided '
'by the publicKeysUrl (KeyValueKeyStore)', () async {
getOverride = (Uri uri) async {
return Response(
'{"123": 456}',
@@ -134,6 +177,36 @@ void main() {
});
});
test(
'throws exception if invalid keys are provided '
'by the publicKeysUrl (JwkKeyStore)', () async {
getOverride = (Uri uri) async {
return Response(
'{"keys": 456}',
HttpStatus.ok,
headers: {'cache-control': 'max-age=3600'},
);
};
await withClock(Clock.fixed(validTime), () async {
await expectLater(
() => verify(
tokenWithNoMatchingKid,
audience: {audience},
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
),
throwsA(
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'''Invalid public keys returned by https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com.''',
),
),
);
});
});
test('can verify an invalid audience', () async {
await withClock(Clock.fixed(validTime), () async {
try {
@@ -144,7 +217,7 @@ void main() {
publicKeysUrl: keyValuePublicKeysUrl,
);
fail('should throw');
} catch (error) {
} on Exception catch (error) {
expect(
error,
isA<JwtVerificationFailure>().having(
@@ -167,7 +240,7 @@ void main() {
publicKeysUrl: keyValuePublicKeysUrl,
);
fail('should throw');
} catch (error) {
} on Exception catch (error) {
expect(
error,
isA<JwtVerificationFailure>().having(
@@ -269,7 +342,7 @@ void main() {
group('JwtVerificationFailure', () {
test('toString is correct', () {
const reason = 'reason';
final failure = JwtVerificationFailure(reason);
const failure = JwtVerificationFailure(reason);
expect(failure.toString(), equals('JwtVerificationFailure: $reason'));
});
});