feat(jwt): add support for JWK-based JWT verification (#1742)

This commit is contained in:
Bryan Oltman
2024-02-20 17:06:36 -05:00
committed by GitHub
parent 5451a9305b
commit 30b1bc2f0c
13 changed files with 520 additions and 188 deletions
+36 -31
View File
@@ -5,29 +5,21 @@ import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:http/http.dart' as http;
import 'package:jwt/jwt.dart';
import 'package:jwt/src/models/public_key_store/public_key_store.dart';
import 'package:meta/meta.dart';
import 'package:pointycastle/pointycastle.dart';
import 'package:rsa_pkcs/rsa_pkcs.dart' as rsa;
import 'package:ttl_cache/ttl_cache.dart';
/// {@template public_key_store}
/// A store for the public keys.
/// {@endtemplate}
class PublicKeyStore {
/// {@macro public_key_store}
const PublicKeyStore({required this.keys, required this.expiration});
/// A mapping of URLs to the public keys they contain.
/// Example URL: https://login.microsoftonline.com/common/discovery/v2.0/keys
@visibleForTesting
final publicKeyStores = TtlCache<String, PublicKeyStore>();
/// Map of all public key id/value pairs.
final Map<String, String> keys;
/// Expiration time.
final DateTime expiration;
}
PublicKeyStore? _publicKeyStore;
Future<Map<String, String>> _getPublicKeys(String url) async {
if (_publicKeyStore?.expiration.isAfter(clock.now()) ?? false) {
return _publicKeyStore!.keys;
Future<PublicKeyStore?> _getPublicKeys(String url) async {
final store = publicKeyStores.get(url);
if (store != null) {
return store;
}
final get = getOverride ?? http.get;
@@ -39,19 +31,23 @@ Future<Map<String, String>> _getPublicKeys(String url) async {
final maxAgeRegExp = RegExp(r'max-age=(\d+)');
final match = maxAgeRegExp.firstMatch(response.headers['cache-control']!);
final maxAge = int.parse(match!.group(1)!);
final publicKeys = (json.decode(response.body) as Map<String, dynamic>)
.cast<String, String>();
_publicKeyStore = PublicKeyStore(
keys: publicKeys,
expiration: clock.now().add(Duration(seconds: maxAge)),
final publicKeyStore = PublicKeyStore.tryDeserialize(
json.decode(response.body) as Map<String, dynamic>,
);
return publicKeys;
}
if (publicKeyStore == null) {
return null;
}
/// Typedef for a function that returns the public keys asynchronously.
typedef GetPublicKeys = Future<Map<String, String>> Function();
publicKeyStores.set(
url,
publicKeyStore,
ttl: Duration(seconds: maxAge),
);
return publicKeyStore;
}
/// {@template jwt_verification_failure}
/// An exception thrown during JWT verification.
@@ -82,11 +78,20 @@ Future<Jwt> verify(
}
final publicKeys = await _getPublicKeys(publicKeysUrl);
if (publicKeys == null) {
throw JwtVerificationFailure(
'Invalid public keys returned by $publicKeysUrl.',
);
}
await _verifyHeader(jwt.header, publicKeys);
await _verifyHeader(jwt.header, publicKeys.keyIds);
_verifyPayload(jwt.payload, issuer, audience);
final isValid = _verifySignature(encodedJwt, publicKeys[jwt.header.kid]!);
// 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 isValid = _verifySignature(encodedJwt, publicKey);
if (!isValid) {
throw const JwtVerificationFailure('Invalid signature.');
}
@@ -97,7 +102,7 @@ Future<Jwt> verify(
Future<void> _verifyHeader(
JwtHeader header,
Map<String, dynamic> publicKeys,
Iterable<String> keyIds,
) async {
if (header.typ != 'JWT') {
throw const JwtVerificationFailure('Invalid token type.');
@@ -107,7 +112,7 @@ Future<void> _verifyHeader(
throw const JwtVerificationFailure('Invalid algorithm.');
}
if (!publicKeys.containsKey(header.kid)) {
if (!keyIds.contains(header.kid)) {
throw const JwtVerificationFailure('Invalid key id.');
}
}
+44
View File
@@ -0,0 +1,44 @@
import 'package:json_annotation/json_annotation.dart';
part 'jwk.g.dart';
/// {@template jwk}
/// A JSON Web Key (JWK) (https://datatracker.ietf.org/doc/html/rfc7517)
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class Jwk {
/// {@macro jwk}
const Jwk({
required this.kty,
required this.use,
required this.kid,
required this.x5c,
required this.x5t,
required this.n,
required this.e,
});
/// Decodes a JSON object into a JWK.
factory Jwk.fromJson(Map<String, dynamic> json) => _$JwkFromJson(json);
/// Key type
final String kty;
/// Key use
final String use;
/// Key ID
final String kid;
/// X.509 certificate thumbprint
final String x5t;
/// X.509 certificate chain
final List<String> x5c;
/// RSA modulus
final String n;
/// RSA public exponent
final String e;
}
+27
View File
@@ -0,0 +1,27 @@
// 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
part of 'jwk.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Jwk _$JwkFromJson(Map<String, dynamic> json) => $checkedCreate(
'Jwk',
json,
($checkedConvert) {
final val = Jwk(
kty: $checkedConvert('kty', (v) => v as String),
use: $checkedConvert('use', (v) => v as String),
kid: $checkedConvert('kid', (v) => v as String),
x5c: $checkedConvert('x5c',
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
x5t: $checkedConvert('x5t', (v) => v as String),
n: $checkedConvert('n', (v) => v as String),
e: $checkedConvert('e', (v) => v as String),
);
return val;
},
);
@@ -0,0 +1,40 @@
import 'package:collection/collection.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:jwt/src/models/jwk.dart';
import 'package:jwt/src/models/public_key_store/public_key_store.dart';
part 'jwk_key_store.g.dart';
/// {@template jwk_key_store}
/// A collection of JSON Web Keys (JWKs). Microsoft uses this format.
///
/// Ex: https://login.microsoftonline.com/common/discovery/v2.0/keys
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class JwkKeyStore extends PublicKeyStore {
/// {@macro jwk_key_store}
JwkKeyStore({required this.keys});
/// The collection of JWKs.
final List<Jwk> keys;
/// Decodes a JSON object into a [JwkKeyStore].
static JwkKeyStore fromJson(Map<String, dynamic> json) =>
_$JwkKeyStoreFromJson(json);
@override
Iterable<String> get keyIds => keys.map((key) => key.kid);
@override
String? getPublicKey(String kid) {
final key = keys.firstWhereOrNull((key) => key.kid == kid)?.x5c.firstOrNull;
if (key == null) {
return null;
}
return '''
-----BEGIN CERTIFICATE-----
$key
-----END CERTIFICATE-----''';
}
}
@@ -0,0 +1,24 @@
// 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
part of 'jwk_key_store.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
JwkKeyStore _$JwkKeyStoreFromJson(Map<String, dynamic> json) => $checkedCreate(
'JwkKeyStore',
json,
($checkedConvert) {
final val = JwkKeyStore(
keys: $checkedConvert(
'keys',
(v) => (v as List<dynamic>)
.map((e) => Jwk.fromJson(e as Map<String, dynamic>))
.toList()),
);
return val;
},
);
@@ -0,0 +1,24 @@
import 'package:jwt/src/models/public_key_store/public_key_store.dart';
/// {@template key_value_key_store}
/// A store for public keys of the form `id: value`. Google uses this format.
///
/// Ex: https://www.googleapis.com/oauth2/v1/certs
/// {@endtemplate}
class KeyValueKeyStore extends PublicKeyStore {
/// {@macro key_value_key_store}
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)));
/// Map of all public key id/value pairs.
final Map<String, String> keys;
@override
Iterable<String> get keyIds => keys.keys;
@override
String? getPublicKey(String kid) => keys[kid];
}
@@ -0,0 +1,33 @@
import 'package:jwt/src/models/public_key_store/jwk_key_store.dart';
import 'package:jwt/src/models/public_key_store/key_value_key_store.dart';
/// {@template public_key_store}
/// A store for the public keys.
/// {@endtemplate}
abstract class PublicKeyStore {
/// {@macro public_key_store}
const PublicKeyStore();
/// Attempts to deserialize a [PublicKeyStore] from a JSON object. This will
/// return the appropriate subclass of [PublicKeyStore] if the JSON object
/// contains the necessary fields.
static PublicKeyStore? tryDeserialize(Map<String, dynamic> json) {
if (json.containsKey('keys')) {
try {
return JwkKeyStore.fromJson(json);
} catch (_) {}
} else {
try {
return KeyValueKeyStore.fromJson(json);
} catch (_) {}
}
return null;
}
/// The key IDs contained in this store.
Iterable<String> get keyIds;
/// Returns a public key for the given key ID, if one exists.
String? getPublicKey(String kid);
}
+3
View File
@@ -8,14 +8,17 @@ environment:
dependencies:
clock: ^1.1.0
collection: ^1.18.0
http: ^1.0.0
json_annotation: ^4.4.0
meta: ^1.7.0
pointycastle: ^3.5.0
rsa_pkcs: ^2.0.0
ttl_cache: ^0.3.0
dev_dependencies:
build_runner: ^2.0.0
json_serializable: ^6.1.4
path: ^1.9.0
test: ^1.19.2
very_good_analysis: ^5.1.0
+76
View File
@@ -0,0 +1,76 @@
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "kWbkaa6qs8wsTnBwiiNYOhHbnAw",
"x5t": "kWbkaa6qs8wsTnBwiiNYOhHbnAw",
"n": "t6Q2XSeWnMA_-crH2UbftfS01QDAqHoPQFqsRtVkxG4eyamnNlTl3Da07QQkjpPEbLoLtgtMI2Pr0plO7xU9f94mhbfK_UJ6Y0KcWxhwKMkCgnzcFOQF4eH_AICHLOKa8vPthtcprNcCmjbksW5TYBZi6uLhFLw_HsjGOxhK0VaDWnWizNVeqvzVB0jt9Vdmfhs6Zohy_1b2Wusdad1NmSKzhC74IDjlIaFoik_ZJJdtLOgoIwOZTLW0M1UKhRrWtj7AjVCnE_zBiloACm1IrIM_PymE10cJJ6WFz29ep4g7X65xCEU6zJ5oIFibvk6cKKcFNB7FFjbehYVpw5BxVQ",
"e": "AQAB",
"x5c": [
"MIIC/TCCAeWgAwIBAgIICHb5qy8hKKgwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDAxMTUxODA0MTRaFw0yOTAxMTUxODA0MTRaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3pDZdJ5acwD/5ysfZRt+19LTVAMCoeg9AWqxG1WTEbh7Jqac2VOXcNrTtBCSOk8Rsugu2C0wjY+vSmU7vFT1/3iaFt8r9QnpjQpxbGHAoyQKCfNwU5AXh4f8AgIcs4pry8+2G1yms1wKaNuSxblNgFmLq4uEUvD8eyMY7GErRVoNadaLM1V6q/NUHSO31V2Z+GzpmiHL/VvZa6x1p3U2ZIrOELvggOOUhoWiKT9kkl20s6CgjA5lMtbQzVQqFGta2PsCNUKcT/MGKWgAKbUisgz8/KYTXRwknpYXPb16niDtfrnEIRTrMnmggWJu+TpwopwU0HsUWNt6FhWnDkHFVAgMBAAGjITAfMB0GA1UdDgQWBBQLGQYqt7pRrKWQ25XWSi6lGN818DANBgkqhkiG9w0BAQsFAAOCAQEAtky1EYTKZvbTAveLmL3VCi+bJMjY5wyDO4Yulpv0VP1RS3dksmEALOsa1Bfz2BXVpIKPUJLdvFFoFhDqReAqRRqxylhI+oMwTeAsZ1lYCV4hTWDrN/MML9SYyeQ441Xp7xHIzu1ih4rSkNwrsx231GTfzo6dHMsi12oEdyn6mXavWehBDbzVDxbeqR+0ymhCgeYjIfCX6z2SrSMGYiG2hzs/xzypnIPnv6cBMQQDS4sdquoCsvIqJRWmF9ow79oHhzSTwGJj4+jEQi7QMTDR30rYiPTIdE63bnuARdgNF/dqB7n4ZJv566jvbzHpfCTqrJyj7Guvjr9i56NpLmz2DA=="
],
"issuer": "https://login.microsoftonline.com/{tenantid}/v2.0"
},
{
"kty": "RSA",
"use": "sig",
"kid": "XRvko8P7A3UaWSnU7bM9nT0MjhA",
"x5t": "XRvko8P7A3UaWSnU7bM9nT0MjhA",
"n": "vRIL3aZt-xVqOZgMOr71ltWe9YY2Wf_B28C4Jl2nBSTEcFnf_eqOHZ8yzUBbLc4Nti2_ETcCsTUNuzS368BWkSgxc45JBH1wFSoWNFUSXaPt8mRwJYTF0H32iNhw_tBb9mvdQVgVs4Ci0dVJRYiz-ilk3PeO8wzlwRuwWIsaKFYlMyOKG9DVFbg93DmP5Tjq3C3oJlATyhAiJJc1T2trEP8960an33dDEaWwVAHh3c_34meAO4R6kLzIq0JnSsZMYB9O_6bMyIlzxmdZ8F442SynCUHxhnIh3yZew-xDdeHr6Ofl7KeVUcvSiZP9X44CaVJvknXQbBYNl-H7YF5RgQ",
"e": "AQAB",
"x5c": [
"MIIC/jCCAeagAwIBAgIJAKysonliFZLIMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwMjA4MTcwMjUzWhcNMjkwMjA4MTcwMjUzWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvRIL3aZt+xVqOZgMOr71ltWe9YY2Wf/B28C4Jl2nBSTEcFnf/eqOHZ8yzUBbLc4Nti2/ETcCsTUNuzS368BWkSgxc45JBH1wFSoWNFUSXaPt8mRwJYTF0H32iNhw/tBb9mvdQVgVs4Ci0dVJRYiz+ilk3PeO8wzlwRuwWIsaKFYlMyOKG9DVFbg93DmP5Tjq3C3oJlATyhAiJJc1T2trEP8960an33dDEaWwVAHh3c/34meAO4R6kLzIq0JnSsZMYB9O/6bMyIlzxmdZ8F442SynCUHxhnIh3yZew+xDdeHr6Ofl7KeVUcvSiZP9X44CaVJvknXQbBYNl+H7YF5RgQIDAQABoyEwHzAdBgNVHQ4EFgQU8Sqmrf0UFpZbGtl5y1CjUdQq5ycwDQYJKoZIhvcNAQELBQADggEBAA57FiIOUs5yyLD6a6rWCbQ4Z2XJTfQb+TM/tZ6V6QqNhSS+Q98KFOIWe9Sit0iAyDsCCKuA8f08PYnUiHmHq8dG/7YRTShE/3zCZXHYKJgMaBhYfS788zQuq/hXDdVVc5X0pZwM4ibc6+2XpcpeDHxpMOLwo2AwujDdHVLzedAkIaTCzwPIizP4LB6l6IxR+xRXsH/1f034Gk3ReAEGgHW12NkajtXmC3DKl6vGIHvx1PgAMWQbxq3F2OopNx6aZEIIZWcMpQZ6/62f3pxRJHzZiJZN+khV8hpNjJvCNf6/hNbxkLcjLAycjW8AttcCRSTM4F+02S3TyHmoE4pYywA="
],
"issuer": "https://login.microsoftonline.com/{tenantid}/v2.0"
},
{
"kty": "RSA",
"use": "sig",
"kid": "fwNs8F_h9KrHMA_OcTP1pGFWwyc",
"x5t": "fwNs8F_h9KrHMA_OcTP1pGFWwyc",
"n": "6Jiu4AU4ZWHBFEbO1-41P6dxKgGx7J31i5wNzH5eoJlsNjWrWoGlZip8ey_ZppcNMY0GY330p8YwdazqRX24mPkyOxbYF1uGEGB_XtmMOtuG45WyPlbARQl8hok7y_hbydS8uyfm_ZQXN7MLgju0f4_cYo-dgic5OaR3W6CWfgOrNnf287ZZ2HtJ8DZNm-oHE2_Tg9FFTIIkpltNIZ4rJ0uwzuy7zkep_Pfxptzmpd0rwd0F87IneYu-jtKUvHVVPJQ7yQvgin0rZR8tXIp_IzComGipktu_AJ89z3atOEt0_vZPizQIMRpToHjUTNXuXaDWIvCIJYMkvvl0HJxf1Q",
"e": "AQAB",
"x5c": [
"MIIC6TCCAdGgAwIBAgIIV6K/4n2M5VAwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMB4XDTIzMTEzMDAwMTAxNVoXDTI4MTEzMDAwMTAxNVowIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Jiu4AU4ZWHBFEbO1+41P6dxKgGx7J31i5wNzH5eoJlsNjWrWoGlZip8ey/ZppcNMY0GY330p8YwdazqRX24mPkyOxbYF1uGEGB/XtmMOtuG45WyPlbARQl8hok7y/hbydS8uyfm/ZQXN7MLgju0f4/cYo+dgic5OaR3W6CWfgOrNnf287ZZ2HtJ8DZNm+oHE2/Tg9FFTIIkpltNIZ4rJ0uwzuy7zkep/Pfxptzmpd0rwd0F87IneYu+jtKUvHVVPJQ7yQvgin0rZR8tXIp/IzComGipktu/AJ89z3atOEt0/vZPizQIMRpToHjUTNXuXaDWIvCIJYMkvvl0HJxf1QIDAQABoyEwHzAdBgNVHQ4EFgQUTtiYd3S6DOacXBmYsKyr1EK67f4wDQYJKoZIhvcNAQELBQADggEBAGbSzomDLsU7BX6Vohf/VweoJ9TgYs4cYcdFwMrRQVMpMGKYU6HT7f8mDzRGqpursuJTsN9yIOk7s5xp+N7EL6XKauzo+VHOGJT1qbwzJXT1XY6DuzBrlhtY9C7AUHlpYAD4uWyt+JfuB+z5Qq5cbGr0dMS/EEKR/m0iBboUNDji6u9sUzGqUYn4tpBoE+y0J8UttankG/09PPHwQIjMxMXcBDmGi5VTp9eY5RFk9GQ4qdQJUp2hhdQZDVpz6lcPxhG92RPO/ca3P/9dvfI5aNaiSyV7vuK2NGCVGCTeo/okA+V5dm5jeuf0bupNEPnXSGyM8EHjcRjR+cHsby5pIGs="
],
"issuer": "https://login.microsoftonline.com/{tenantid}/v2.0"
},
{
"kty": "RSA",
"use": "sig",
"kid": "qor_VePWgmxWy3r1dpfsWsw2-zY",
"x5t": "qor_VePWgmxWy3r1dpfsWsw2-zY",
"n": "rV8eXna9NCyzvgVZvbz18NhLIAfo1Qzn-VQQCbQzyGi2KDe3RI2sLeHltv9mVI2sahcRjgvhYNSETyxqHaKw3w8L4jg0kJdfzhD8dvpl32hunOCzuY2WpyJVq6CkxzGN4iikWTEIe_GMGsu9qhdxybaTCBTAya8qyKL1sbEByk8FiY6nsm6BhuRUVCh_rzfAp3HY-U_58ORLF1tmZrmSljHMFwlxvYuOIlKHacXy9gen8HsT7PUSA4n2PdnT1XAmlKJG1mzvdqyG2L3iRQJ45tcmrERKcd1pYwhb7ZtTyKypkeR9lkKbaYiQUt1QhpeO12pH1bRB1_k9MMzOm8Ca1Q",
"e": "AQAB",
"x5c": [
"MIIC6jCCAdKgAwIBAgIJAMqvEglnjttEMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDAxMjQyMjE0MjNaFw0yOTAxMjQyMjE0MjNaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1fHl52vTQss74FWb289fDYSyAH6NUM5/lUEAm0M8hotig3t0SNrC3h5bb/ZlSNrGoXEY4L4WDUhE8sah2isN8PC+I4NJCXX84Q/Hb6Zd9obpzgs7mNlqciVaugpMcxjeIopFkxCHvxjBrLvaoXccm2kwgUwMmvKsii9bGxAcpPBYmOp7JugYbkVFQof683wKdx2PlP+fDkSxdbZma5kpYxzBcJcb2LjiJSh2nF8vYHp/B7E+z1EgOJ9j3Z09VwJpSiRtZs73ashti94kUCeObXJqxESnHdaWMIW+2bU8isqZHkfZZCm2mIkFLdUIaXjtdqR9W0Qdf5PTDMzpvAmtUCAwEAAaMhMB8wHQYDVR0OBBYEFKOxwRo5B0oCCCLMp8I/cHosF4cPMA0GCSqGSIb3DQEBCwUAA4IBAQCifXD9bW3dh6WAwfFHnL2HefUhuDQzisAZBR0o6kPASObicJK91BtfVg6iYW0LUCE70EVnFkyypTy19JIPf3zutQkHFAdXtS2/0NiR0vRJ561gi5Yqjl9BW9Az6Eb/O4UEzqBpe313FNt2co8I0OjRNhbKB1lIPUu6UZs5qBdfTwQFB6fU/XfXHnpZERZgRUZu5mku/n2EHZ1iMe9of1Qv/AtXgB51ZlfpT6YbrqMJBJs1yHxLd+rYqoXCwWLoBlJ3xYm4jEzSHPLjFgqHrUb9Cl2SazRKhV/UBAGqq0xG6qZMoiWvcfv0equddGa/r84lrEU2y1RBGUGw15jiLL/y"
],
"issuer": "https://login.microsoftonline.com/{tenantid}/v2.0"
},
{
"kty": "RSA",
"use": "sig",
"kid": "2Spohh9y2me52nKrhai7GxWJibU",
"x5t": "2Spohh9y2me52nKrhai7GxWJibU",
"n": "nwLh9Ii-9iSmvLT9WiMxSb7MIv__um6y_ying_Rz9WzuP9MDAPxsGw26aiC6W_BieXd5WKJy3h03t5xNQL-oUyC74VOfD_pmwKdzdiY_qn6ERy8HrxHp8HDRMNSYYYDGXPdkrM8rFtiixW6wo15rnGwtrKNBPQPH-S8W6sGbY-pcXehbayMIcHhAs0ywn7fGPZ5X8LIxOtucVmEILs2BcPIhqGEYKuY9C_Cy-KWZiKbg72E-Fp5erUIlYsA85bC6Yyb5Yw9yhhjmqJ3m5B7jjrPqPc0cd_x8ZaVW0fSaHz8TtOYsNbN5qW1ajDvzDZkl6fba0SkwaNmJHLEdubeXEQ",
"e": "AQAB",
"x5c": [
"MIIDAzCCAeugAwIBAgIJAJZMbrNaGyxWMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTAeFw0yNDAxMTkxNzExNDRaFw0yOTAxMTkxNzExNDRaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ8C4fSIvvYkpry0/VojMUm+zCL//7pusv8op4P0c/Vs7j/TAwD8bBsNumogulvwYnl3eViict4dN7ecTUC/qFMgu+FTnw/6ZsCnc3YmP6p+hEcvB68R6fBw0TDUmGGAxlz3ZKzPKxbYosVusKNea5xsLayjQT0Dx/kvFurBm2PqXF3oW2sjCHB4QLNMsJ+3xj2eV/CyMTrbnFZhCC7NgXDyIahhGCrmPQvwsvilmYim4O9hPhaeXq1CJWLAPOWwumMm+WMPcoYY5qid5uQe446z6j3NHHf8fGWlVtH0mh8/E7TmLDWzealtWow78w2ZJen22tEpMGjZiRyxHbm3lxECAwEAAaMuMCwwCwYDVR0PBAQDAgLEMB0GA1UdDgQWBBRXpNiRf2930gemHXJQsfP+RQz+7TANBgkqhkiG9w0BAQsFAAOCAQEATvXhuCEg8l8rvy8CdD8hDPpqBMr+8ynpqWKeUBEdsNqXilOSgRxTgEespB9ahFzqFUxXEZF0SqK4TkDDL5Cze8zo4H+DX7WnTyySHD+dxwYNdU8hV+kpA6UH5QSN+zOE7TtDWH0NR00987tXEht16ZvrPR3vNkkEG5sORMGLP20lTVgod+ycTvl8hfDAYagI3qqmfUtPwr2rONoTXDbV2ETlSGnpwq1feddyzWvfOJT/UHVIKO93JsrALrIZ8lXqYvZ2GiRffiuwpD5/QATsLFfs+CR698cxIqyOxuzx3NVqs4rdabUGO2RH5tWzODdFshkaM3u0Uk8hgmGwraJH5Q=="
],
"issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"
},
{
"kty": "RSA",
"use": "sig",
"kid": "tR4wkYhHxHo_qeprS08Cuw8xcOw",
"x5t": "tR4wkYhHxHo_qeprS08Cuw8xcOw",
"n": "o9UAgT4PVXQm-SFWuQeG4OyJjfbLHb-u4rMCH0mggLqRIKvksK3cEMerrjLr8c-OCKkcY9_1JFOcIRPDW11dYyvGsjb54mOZMJAkxkzLvwrUtZrL5HDMPDsUgBBJZsS3_Kw8P9YIm1tdep-b2p6i-0AxPi6XM-UWT-WdHzCbrzZc3dHP5sLDX4Qnp-cICI__U2jLns64oC8AUPlbvXWS9y_iFT7UUV5UJUx0wNsMiKKK12Z_TDg_k_QPHhcInZGs1WWd_0mkktkGIBj4Z2ERpPzRwa8uSwhgfg7RPFku2YdcuirHZ3rPtFl2vUSC5kd7rQLrYXnC8OX77Ds9BcjbTQ",
"e": "AQAB",
"x5c": [
"MIIDAzCCAeugAwIBAgIJALC8zLVLav9eMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTAeFw0yNDAyMDQyMzQ1NTlaFw0yOTAyMDQyMzQ1NTlaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKPVAIE+D1V0JvkhVrkHhuDsiY32yx2/ruKzAh9JoIC6kSCr5LCt3BDHq64y6/HPjgipHGPf9SRTnCETw1tdXWMrxrI2+eJjmTCQJMZMy78K1LWay+RwzDw7FIAQSWbEt/ysPD/WCJtbXXqfm9qeovtAMT4ulzPlFk/lnR8wm682XN3Rz+bCw1+EJ6fnCAiP/1Noy57OuKAvAFD5W711kvcv4hU+1FFeVCVMdMDbDIiiitdmf0w4P5P0Dx4XCJ2RrNVlnf9JpJLZBiAY+GdhEaT80cGvLksIYH4O0TxZLtmHXLoqx2d6z7RZdr1EguZHe60C62F5wvDl++w7PQXI200CAwEAAaMuMCwwCwYDVR0PBAQDAgLEMB0GA1UdDgQWBBQo2/CFr1BU51goqshFZY5x3UWkJTANBgkqhkiG9w0BAQsFAAOCAQEAYlOjSt0yAkL2f0vtvbm0pMYP9RE1daJRh0oxAlpNDA4vNsItHmbb507FH+ce5tQf7vYHsamdY7cf9lQGFZPlM4Wb2j2zfGF+6cfBSTXNYEkTxTV/iJwwbVVWE9RKbXQm2zTN1q+U50m4RNRQBwyNfZSurjJhagU6XZRck47yZpt3SEsvVOCWklm2aOlpBVfr5j1z4vxbVPIzp35qNXxmXZtFerH3dM9/zOFpVgrmj0g9/RTKm943zybL5UZ5sjwD87N3mloHN74zVPV9yyMP2tPpavdmAAMOcti3sGfhtTzrcKAQm7qKkHoFWtFV9E4oCbIZabgs6YZZIz/yd2OjKw=="
],
"issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"55c188a83546fc188e51576ba72836e0600e8b73": "-----BEGIN CERTIFICATE-----\nMIIDJzCCAg+gAwIBAgIJAIIB9712PS28MA0GCSqGSIb3DQEBBQUAMDYxNDAyBgNV\nBAMMK2ZlZGVyYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20w\nHhcNMjQwMjE2MDQzODE3WhcNMjQwMzAzMTY1MzE3WjA2MTQwMgYDVQQDDCtmZWRl\ncmF0ZWQtc2lnbm9uLnN5c3RlbS5nc2VydmljZWFjY291bnQuY29tMIIBIjANBgkq\nhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq0CrF3x3aYsjr0YOLMOAhEGMvyFp6o4R\nqyEdUrnTDYkhZbcud+fJEQafCTnjS9QHN1IjpuK6gpx5i3+Z63vRjs5EQX7lP1jG\n8Qg+CnBdTTLw4uJi7RmmlKPsYaO1DbNkFO2uEN62sOOzmJCh1od3CZXI1UYH5cvZ\n/sLJaN2A4TwvUTU3aXlXbUNJz/Hy3l0q1Jjta75NrJtJ7Pfj9tVXs8qXp15tZXrn\nbaM+AI0puswt35VsQbmLwUovFFGeToo5q2c/c1xYnV5uQYMadANekGPRFPM9JZpS\nSIvH0Lv/f15V2zRqmIgX7a3RcmTnr3+w3QNQTogdy+MogxPUdRbxowIDAQABozgw\nNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggr\nBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEADxIU6PJPtY8kNFcf5JhFrcYXzdNg\nHk+EypUtGnEuulcyAIq43VkFg570koFHd/zToq1YemmKyPDoS6Nj2eJrvvBv3/yf\nnTKTepfnH2LrLGicRsYB7kjbWULFR80Xd3tDAiXP5CYdkkeENChkXADZTGrTm9J5\nzOJ/DeltyNp9MgwwdjMFHplS5SVVMzWI5OkjG1K+PTCTkt2oBKf43RaKPX+k2BgI\nkNVMLsDAlWcMrBQiPhgAek+sRaJByUZEp38pFCNSf/Ld4fz5ovFw+h/Nq8VQWoOI\njukxIwcvF/DV6FEDjakwijRkWV6Yc4ejauBT6JduSHeqiI8AIQh7Q8LdiQ==\n-----END CERTIFICATE-----\n",
"c10c90ba4c3663516e0707d0e9a8948106285158": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyCtJXKQDbTq6Cp/zqmTO\nBF2+K1aRYevhUglKCcLMJ0P16LJEIznnOIB0SZuP2jcFHwiMZ9PyzpocOAPUZZue\nrFxKyxcCP8zS/mpHwrSxpxlgm/4oChvYW7U4rH7E/Hb4r2iMqlkgRsCnyvN/EKdG\ne1TkVNnK3dcX+CFgGOjkhAQc5/dz9pThb5oGM36kfnHSlkprGZtS+ijYXsqMJWr7\n410uNaVjFZnp53B9atcWhnfQ4ev3elR0TV+2TZIXd5n/wF2aX44dSB2KQE3aRDSX\n4dZHp/a3xWeeLhcfLkKe5HYUSPgx91742y6TLJd1stOWTg+y2G1A1kaDBEaUVtRV\nFwIDAQAB\n-----END PUBLIC KEY-----",
"ed806f1842b588054b18b669dd1a09a4f367afc4": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIW48NmivYJM0wDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAwwrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0yNDAyMDgwNDM4MTZaFw0yNDAyMjQxNjUzMTZaMDYxNDAyBgNVBAMMK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsfdDk1jowB5oTw25LDtHDYJzV7+P6RRLT\nji275ZyZMXMEpO4q8iWCPnDRKVJuJWXzXMG161AW27CQzNaCcKSz7b4XCmjL//Y1\nSi5SAu0iCABUTI4aA8t/JqzC2RKZmxSZZaD0nwKAxR2hcWRtkA+Vp0NyZKqJcnty\nn2e+ujp/iyN+sYNjPn/5I3PUBAQZu9xOH0R5mS8o+uadXdgRH3lsmyCQeY0lWRDo\ntxa0vIqFIZAZEx5NhpcNqNzYqspPed63uo2FsCbsqlZ11u1HmXoDRNEKsAyhXYzr\nz8YtEXbhpyPdQ7lFbRKS5oFNlLgpCnKjIAEIC2MtKv7dFjpZH2enAgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQAlr+3uEpFMb9uLPYAB7ZhhjMrf4LXY\nmUEhZKcUwPbRXey26f6k7izYBSi/jg1Jka3o6ic/7O+912oHXfOqR+4sNCJue2tk\njvHKqadA7hx8Y0nyDEeJE6MnuNW8o0LF6MKKfBHVJ0Suo37tl5guPsvyviG8F1Ps\nDnOU4ra1bQDIziUTaC9VlEEQgfiEvomAJdjW94GRPEAi8xXM3cjHoQH+Nbv/Of5o\nG2qiYu4RIa8h7yGsRO2wl+9pzrSXjfFttsGiW/W8tSgkXond3IWLcmGLjHIfIcY1\n+r9qQRnPFgStVk0anIxarlUdCm3cLJfvM5H0ZhvkU4D5+9XgzeIKnbXc\n-----END CERTIFICATE-----\n"
}
-9
View File
@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyCtJXKQDbTq6Cp/zqmTO
BF2+K1aRYevhUglKCcLMJ0P16LJEIznnOIB0SZuP2jcFHwiMZ9PyzpocOAPUZZue
rFxKyxcCP8zS/mpHwrSxpxlgm/4oChvYW7U4rH7E/Hb4r2iMqlkgRsCnyvN/EKdG
e1TkVNnK3dcX+CFgGOjkhAQc5/dz9pThb5oGM36kfnHSlkprGZtS+ijYXsqMJWr7
410uNaVjFZnp53B9atcWhnfQ4ev3elR0TV+2TZIXd5n/wF2aX44dSB2KQE3aRDSX
4dZHp/a3xWeeLhcfLkKe5HYUSPgx91742y6TLJd1stOWTg+y2G1A1kaDBEaUVtRV
FwIDAQAB
-----END PUBLIC KEY-----
+200 -148
View File
@@ -1,10 +1,10 @@
// ignore_for_file: prefer_const_constructors
import 'dart:convert';
import 'dart:io';
import 'package:clock/clock.dart';
import 'package:http/http.dart';
import 'package:jwt/jwt.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
void main() {
@@ -12,181 +12,233 @@ void main() {
'''eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxMGM5MGJhNGMzNjYzNTE2ZTA3MDdkMGU5YTg5NDgxMDYyODUxNTgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwiYXV0aF90aW1lIjoxNjQzNjg0MjY2LCJ1c2VyX2lkIjoiRzR1MzdXdk90dmVWR0pRb1pCWGpxcHVWazZWMiIsInN1YiI6Ikc0dTM3V3ZPdHZlVkdKUW9aQlhqcXB1Vms2VjIiLCJpYXQiOjE2NDM2ODQyNjYsImV4cCI6MTY0MzY4Nzg2NiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3RAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.bUWnX_XmR1d9EmeFeYSsK_CHU1u9NPIHgyaQueZ6urYOtxvuL_QodjPl0c9CBJwctwPnxVyRmkeNCw0oF9xBgph0NApLL4FIG6vpDPZfW9txZBYr8xIvaqvmD0diACENAQdjRT2XmyEdQ2-U7SsTonybHmLoU9FMQTjAgw4NCALQvExfB6rtQ9GDsOBt1xoBkB3Vo7a5OmugZ1aHXF69b8As6137-Dggf5qx5R3oLRFovICMMesQziE3vGi-WKcbQxSeiD-9a6ShPAhk41XiyjFGDEOtUCQo63uwQnMw3g0KVtC6bzIyFq-E91vhxumxXzxPYC-kg7iUYiSZy7Y-Aw''';
const tokenNoAuthTime =
'''eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxMGM5MGJhNGMzNjYzNTE2ZTA3MDdkMGU5YTg5NDgxMDYyODUxNTgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwidXNlcl9pZCI6Ikc0dTM3V3ZPdHZlVkdKUW9aQlhqcXB1Vms2VjIiLCJzdWIiOiJHNHUzN1d2T3R2ZVZHSlFvWkJYanFwdVZrNlYyIiwiaWF0IjoxNjQzNjg0MjY2LCJleHAiOjE2NDM2ODc4NjYsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJ0ZXN0QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.ZWCadE43mUk43cPQdNCCi4WhDgB4ZsDT9rhPGQq_1uFPhzkVrCSRcjUhwkzH11VLap_MVurNvI_pGWbu9Z4CRPvGFzXPpuNveWy2qFPEa4jcM-R40vsbrP30vNnrp4PrmqgLar0vWs6FZ2g9fbjU8L1LaU5ik31OKSXufTIKn_hPHhyIC33tYTpWzG3Abq3H9EELHUXKW9nEcN8YYnOHAZ3A6ymb3DyBguhf2O-XAIlrn1WoxRRqlukFGSmprk7heonbVUTzoc3sIDZcC-Cj1U9wTee1NmqmU7v3SvpBRGnuXz-5rzSRHblyVxn_EEfCYwjsDUwetYpyFcCs5dqPlQ''';
const publicKeysUrl =
const tokenWithNoMatchingKid =
'''eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwidXNlcl9pZCI6Ikc0dTM3V3ZPdHZlVkdKUW9aQlhqcXB1Vms2VjIiLCJzdWIiOiJHNHUzN1d2T3R2ZVZHSlFvWkJYanFwdVZrNlYyIiwiaWF0IjoxNjQzNjg0MjY2LCJleHAiOjE2NDM2ODc4NjYsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJ0ZXN0QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.RBAwg-Ttf36aDkVR97Rd50aMp-0yzk8do_4AUPFi9nEhKu0Ye8ox_9hdTBttJYxoOq0NbH2zz0JHSnSHqoTvCleVhoGqg8YghzH0NiqncPfDzi-IcRfy2K8CrOoXuqXaj3YWrbrzNWAYV46eFmvI2TmPO55AyFOjhvLpW-uf96ceOPjueZm8o5K2DZym86BAhSdShknONV2O7b2vW34TXf3UJdISs5p9z6Si4JOWGjbVPY45CO16ODdDxyUGUM2-IVQloB7bg0nMqQbIjKDoO5g9d1nguR2Z-YBiv0BX1MDWDlrBsMAzzRVaFlf6YQS1LKxu2tnMdCrBnK_Wvqt5sQ''';
const keyValuePublicKeysUrl =
'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
const issuer = 'https://securetoken.google.com/my-app';
const audience = 'my-app';
final publicKey = File('test/fixtures/public_key.pem').readAsStringSync();
final body = json.encode(
{'c10c90ba4c3663516e0707d0e9a8948106285158': publicKey},
);
const jwkPublicKeysUrl =
'https://login.microsoftonline.com/common/discovery/v2.0/keys';
final jwkKeyStoreJsonString =
File(p.join('test', 'fixtures', 'jwk_key_store.json')).readAsStringSync();
final keyValueKeyStoreString =
File(p.join('test', 'fixtures', 'key_value_key_store.json'))
.readAsStringSync();
final expiresAt = DateTime.fromMillisecondsSinceEpoch(1643687866 * 1000);
final validTime = expiresAt.subtract(Duration(minutes: 15));
late String keyStoreResponseBody;
setUp(() {
publicKeyStores.clear();
getOverride = (Uri uri) async {
return Response(
keyStoreResponseBody,
HttpStatus.ok,
headers: {'cache-control': 'max-age=3600'},
);
};
});
group('verify', () {
test('can be instantiated', () {
expect(verify, isNotNull);
});
group('when key store is key-value', () {
const issuer = 'https://securetoken.google.com/my-app';
const audience = 'my-app';
test('can verify an expired jwt', () async {
getOverride = (Uri uri) async {
return Response(
body,
HttpStatus.ok,
headers: {'cache-control': 'max-age=3600'},
);
};
await expectLater(
() => verify(
token,
audience: {audience},
issuer: issuer,
publicKeysUrl: publicKeysUrl,
),
throwsA(
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'Token has expired.',
),
),
);
});
test('throws a JwtVerificationFailure if string is not valid jwt',
() async {
await expectLater(
() => verify(
'not.a.jwt',
audience: {audience},
issuer: issuer,
publicKeysUrl: publicKeysUrl,
),
throwsA(
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'JWT header is malformed.',
),
),
);
});
test('can verify an invalid audience', () async {
await withClock(Clock.fixed(validTime), () async {
getOverride = (Uri uri) async {
return Response(
body,
HttpStatus.ok,
headers: {'cache-control': 'max-age=3600'},
);
};
try {
await verify(
token,
audience: {'invalid-audience'},
issuer: issuer,
publicKeysUrl: publicKeysUrl,
);
fail('should throw');
} catch (error) {
expect(
error,
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'Invalid audience.',
),
);
}
setUp(() {
keyStoreResponseBody = keyValueKeyStoreString;
});
});
test('can verify an invalid issuer', () async {
await withClock(Clock.fixed(validTime), () async {
getOverride = (Uri uri) async {
return Response(
body,
HttpStatus.ok,
headers: {'cache-control': 'max-age=3600'},
);
};
try {
await verify(
test('can verify an expired jwt', () async {
await expectLater(
() => verify(
token,
audience: {audience},
issuer: 'https://invalid/issuer',
publicKeysUrl: publicKeysUrl,
);
fail('should throw');
} catch (error) {
expect(
error,
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
),
throwsA(
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'Invalid issuer.',
'Token has expired.',
),
),
);
});
test('throws a JwtVerificationFailure if string is not valid jwt',
() async {
await expectLater(
() => verify(
'not.a.jwt',
audience: {audience},
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
),
throwsA(
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'JWT header is malformed.',
),
),
);
});
test('throws exception if jwt has no matching public key id', () async {
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 key id.',
),
),
);
}
});
});
});
test('can verify a valid jwt', () async {
await withClock(Clock.fixed(validTime), () async {
test('throws exception if invalid keys are provided by the publicKeysUrl',
() async {
getOverride = (Uri uri) async {
return Response(
body,
'{"123": 456}',
HttpStatus.ok,
headers: {'cache-control': 'max-age=3600'},
);
};
final jwt = await verify(
token,
audience: {audience},
issuer: issuer,
publicKeysUrl: publicKeysUrl,
);
expect(jwt, isA<Jwt>());
});
});
test('can verify a valid jwt (multiple audiences)', () async {
await withClock(Clock.fixed(validTime), () async {
getOverride = (Uri uri) async {
return Response(
body,
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.''',
),
),
);
};
final jwt = await verify(
token,
audience: {'other-audience', audience},
issuer: issuer,
publicKeysUrl: publicKeysUrl,
);
expect(jwt, isA<Jwt>());
});
});
});
test('can verify a valid jwt w/out auth_time', () async {
await withClock(Clock.fixed(validTime), () async {
getOverride = (Uri uri) async {
return Response(
body,
HttpStatus.ok,
headers: {'cache-control': 'max-age=3600'},
test('can verify an invalid audience', () async {
await withClock(Clock.fixed(validTime), () async {
try {
await verify(
token,
audience: {'invalid-audience'},
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
);
fail('should throw');
} catch (error) {
expect(
error,
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'Invalid audience.',
),
);
}
});
});
test('can verify an invalid issuer', () async {
await withClock(Clock.fixed(validTime), () async {
try {
await verify(
token,
audience: {audience},
issuer: 'https://invalid/issuer',
publicKeysUrl: keyValuePublicKeysUrl,
);
fail('should throw');
} catch (error) {
expect(
error,
isA<JwtVerificationFailure>().having(
(e) => e.reason,
'reason',
'Invalid issuer.',
),
);
}
});
});
test('can verify a valid jwt', () async {
await withClock(Clock.fixed(validTime), () async {
final jwt = await verify(
token,
audience: {audience},
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
);
};
final jwt = await verify(
tokenNoAuthTime,
audience: {audience},
issuer: issuer,
publicKeysUrl: publicKeysUrl,
);
expect(jwt, isA<Jwt>());
expect(jwt, isA<Jwt>());
});
});
test('can verify a valid jwt (multiple audiences)', () async {
await withClock(Clock.fixed(validTime), () async {
final jwt = await verify(
token,
audience: {'other-audience', audience},
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
);
expect(jwt, isA<Jwt>());
});
});
test('can verify a valid jwt w/out auth_time', () async {
await withClock(Clock.fixed(validTime), () async {
final jwt = await verify(
tokenNoAuthTime,
audience: {audience},
issuer: issuer,
publicKeysUrl: keyValuePublicKeysUrl,
);
expect(jwt, isA<Jwt>());
});
});
group('when key store is JWK store', () {
const token =
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InRSNHdrWWhIeEhvX3FlcHJTMDhDdXc4eGNPdyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFKclpOMmI0eldUNzhQWFJEN2J6dkxrIiwiYXVkIjoiYzRhZjk1NjYtOGEzNi00MzQ4LWI0MTMtZGFiNjY1Yjg3MTdkIiwiZXhwIjoxNzA4NTQ2MDYwLCJpYXQiOjE3MDg0NTkzNjAsIm5iZiI6MTcwODQ1OTM2MCwiZW1haWwiOiJicnlhbm9sdG1hbkBob3RtYWlsLmNvbSIsInRpZCI6IjkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsImFpbyI6IkRtUE91OTNxdWlSNm9OYVBrMjl1dVFSTkhSN0NIS0V4VDlrSE5DamVJIUdpN3JMQks2SDZYYzdkUWl2ZmRxTVZ5VExuNDMxUWRaTXpPSHQqaDQ2NWViQTlBWE9RdzhOdlM3dzNTbFREWU9iOW52OGh2SHNDTXZqdlJJMzRaWXlPbiE2MHBUcXghWnVmeTF0VnRGMWp6VEVsQmlUTjRrOWJIU0RDUFZaU1BlaXNzYjFLdmhCamc4biFPUSptNWszSmZ3JCQifQ.PhZfpmMmhlW_w9P-EQjcA-DgD0Mxnufxgn6rsKlQYtRn2hmT8uGOlMpMthYIeYLhVPtQqPyHDdW-0GxJ3ROK6prlogvPt8SxY8lSiJ6a4XCNmGop-7MxWGkYhqnMw21TKiSsGIr5LSuD3b_5RK6uBipra0rf819Wlvp6JbXulGSHt9N_qyq8328npCHRYbIDOBcOgDjnPjDFp_pJ99-itCsM8OO0npZJZ64MmlENgEYswnOQYlCShF1HLQwes7uGuw8ZBik-XSu44iJKJ9u4Ai3BjpxYdMFmMZ-B4DpF3uWpFsK-X9tMbi23gwufu5X_6aqtZYRLg052M1SUaBiMUw''';
const audience = 'c4af9566-8a36-4348-b413-dab665b8717d';
const issuer =
'''https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0''';
setUp(() {
keyStoreResponseBody = jwkKeyStoreJsonString;
});
test('can verify a valid jwt', () async {
final time = DateTime(2024, 2, 21);
await withClock(Clock.fixed(time), () async {
final jwt = await verify(
token,
audience: {audience},
issuer: issuer,
publicKeysUrl: jwkPublicKeysUrl,
);
expect(jwt, isA<Jwt>());
});
});
});
});
});
+8
View File
@@ -716,6 +716,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
ttl_cache:
dependency: transitive
description:
name: ttl_cache
sha256: "3c8d00de287cd646032af85411412becac3fd8889b12e5b07c9f8809e66cf132"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
typed_data:
dependency: transitive
description: