diff --git a/packages/jwt/lib/src/jwt.dart b/packages/jwt/lib/src/jwt.dart index b12f8489..ad2563cc 100644 --- a/packages/jwt/lib/src/jwt.dart +++ b/packages/jwt/lib/src/jwt.dart @@ -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(); - /// Map of all public key id/value pairs. - final Map keys; - - /// Expiration time. - final DateTime expiration; -} - -PublicKeyStore? _publicKeyStore; - -Future> _getPublicKeys(String url) async { - if (_publicKeyStore?.expiration.isAfter(clock.now()) ?? false) { - return _publicKeyStore!.keys; +Future _getPublicKeys(String url) async { + final store = publicKeyStores.get(url); + if (store != null) { + return store; } final get = getOverride ?? http.get; @@ -39,19 +31,23 @@ Future> _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) - .cast(); - _publicKeyStore = PublicKeyStore( - keys: publicKeys, - expiration: clock.now().add(Duration(seconds: maxAge)), + final publicKeyStore = PublicKeyStore.tryDeserialize( + json.decode(response.body) as Map, ); - return publicKeys; -} + if (publicKeyStore == null) { + return null; + } -/// Typedef for a function that returns the public keys asynchronously. -typedef GetPublicKeys = Future> 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 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 verify( Future _verifyHeader( JwtHeader header, - Map publicKeys, + Iterable keyIds, ) async { if (header.typ != 'JWT') { throw const JwtVerificationFailure('Invalid token type.'); @@ -107,7 +112,7 @@ Future _verifyHeader( throw const JwtVerificationFailure('Invalid algorithm.'); } - if (!publicKeys.containsKey(header.kid)) { + if (!keyIds.contains(header.kid)) { throw const JwtVerificationFailure('Invalid key id.'); } } diff --git a/packages/jwt/lib/src/models/jwk.dart b/packages/jwt/lib/src/models/jwk.dart new file mode 100644 index 00000000..96960632 --- /dev/null +++ b/packages/jwt/lib/src/models/jwk.dart @@ -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 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 x5c; + + /// RSA modulus + final String n; + + /// RSA public exponent + final String e; +} diff --git a/packages/jwt/lib/src/models/jwk.g.dart b/packages/jwt/lib/src/models/jwk.g.dart new file mode 100644 index 00000000..087a0add --- /dev/null +++ b/packages/jwt/lib/src/models/jwk.g.dart @@ -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 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).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; + }, + ); diff --git a/packages/jwt/lib/src/models/public_key_store/jwk_key_store.dart b/packages/jwt/lib/src/models/public_key_store/jwk_key_store.dart new file mode 100644 index 00000000..05124309 --- /dev/null +++ b/packages/jwt/lib/src/models/public_key_store/jwk_key_store.dart @@ -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 keys; + + /// Decodes a JSON object into a [JwkKeyStore]. + static JwkKeyStore fromJson(Map json) => + _$JwkKeyStoreFromJson(json); + + @override + Iterable 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-----'''; + } +} diff --git a/packages/jwt/lib/src/models/public_key_store/jwk_key_store.g.dart b/packages/jwt/lib/src/models/public_key_store/jwk_key_store.g.dart new file mode 100644 index 00000000..1c9c93cf --- /dev/null +++ b/packages/jwt/lib/src/models/public_key_store/jwk_key_store.g.dart @@ -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 json) => $checkedCreate( + 'JwkKeyStore', + json, + ($checkedConvert) { + final val = JwkKeyStore( + keys: $checkedConvert( + 'keys', + (v) => (v as List) + .map((e) => Jwk.fromJson(e as Map)) + .toList()), + ); + return val; + }, + ); diff --git a/packages/jwt/lib/src/models/public_key_store/key_value_key_store.dart b/packages/jwt/lib/src/models/public_key_store/key_value_key_store.dart new file mode 100644 index 00000000..a24a9171 --- /dev/null +++ b/packages/jwt/lib/src/models/public_key_store/key_value_key_store.dart @@ -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 json) => + KeyValueKeyStore(keys: json.map((k, v) => MapEntry(k, v as String))); + + /// Map of all public key id/value pairs. + final Map keys; + + @override + Iterable get keyIds => keys.keys; + + @override + String? getPublicKey(String kid) => keys[kid]; +} diff --git a/packages/jwt/lib/src/models/public_key_store/public_key_store.dart b/packages/jwt/lib/src/models/public_key_store/public_key_store.dart new file mode 100644 index 00000000..37358258 --- /dev/null +++ b/packages/jwt/lib/src/models/public_key_store/public_key_store.dart @@ -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 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 get keyIds; + + /// Returns a public key for the given key ID, if one exists. + String? getPublicKey(String kid); +} diff --git a/packages/jwt/pubspec.yaml b/packages/jwt/pubspec.yaml index afa3ba81..892e8e14 100644 --- a/packages/jwt/pubspec.yaml +++ b/packages/jwt/pubspec.yaml @@ -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 diff --git a/packages/jwt/test/fixtures/jwk_key_store.json b/packages/jwt/test/fixtures/jwk_key_store.json new file mode 100644 index 00000000..4813044b --- /dev/null +++ b/packages/jwt/test/fixtures/jwk_key_store.json @@ -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" + } + ] +} diff --git a/packages/jwt/test/fixtures/key_value_key_store.json b/packages/jwt/test/fixtures/key_value_key_store.json new file mode 100644 index 00000000..0a4cdd8c --- /dev/null +++ b/packages/jwt/test/fixtures/key_value_key_store.json @@ -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" +} diff --git a/packages/jwt/test/fixtures/public_key.pem b/packages/jwt/test/fixtures/public_key.pem deleted file mode 100644 index a7bf8df9..00000000 --- a/packages/jwt/test/fixtures/public_key.pem +++ /dev/null @@ -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----- diff --git a/packages/jwt/test/src/jwt_test.dart b/packages/jwt/test/src/jwt_test.dart index e97c3ca6..6bd9057d 100644 --- a/packages/jwt/test/src/jwt_test.dart +++ b/packages/jwt/test/src/jwt_test.dart @@ -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().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().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().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().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().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().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()); - }); - }); - 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().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()); + }); }); - }); - 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().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().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()); + expect(jwt, isA()); + }); + }); + + 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()); + }); + }); + + 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()); + }); + }); + + 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()); + }); + }); }); }); }); diff --git a/packages/shorebird_cli/pubspec.lock b/packages/shorebird_cli/pubspec.lock index 3f135f84..a76d9adf 100644 --- a/packages/shorebird_cli/pubspec.lock +++ b/packages/shorebird_cli/pubspec.lock @@ -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: