feat(jwt): add support for JWK-based JWT verification (#1742)
This commit is contained in:
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user