feat: add package:jwt (#225)
This commit is contained in:
@@ -42,6 +42,9 @@ jobs:
|
||||
shorebird_code_push_protocol:
|
||||
- ./.github/actions/dart_package
|
||||
- packages/shorebird_code_push_protocol/**
|
||||
jwt:
|
||||
- ./.github/actions/dart_package
|
||||
- packages/jwt/**
|
||||
|
||||
- uses: dorny/paths-filter@v2
|
||||
name: Build Detection
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
pubspec.lock
|
||||
@@ -0,0 +1,23 @@
|
||||
# jwt
|
||||
|
||||
[![License: MIT][license_badge]][license_link]
|
||||
|
||||
A Dart JWT Library.
|
||||
|
||||
```dart
|
||||
import 'package:jwt/jwt.dart' as jwt;
|
||||
|
||||
Future<void> main() async {
|
||||
// Verify and extract a JWT token.
|
||||
final Jwt token = await jwt.verify(
|
||||
'<TOKEN>',
|
||||
issuer: '<ISSUER>',
|
||||
audience: '<AUDIENCE>',
|
||||
publicKeysUrl: '<PUBLIC_KEYS_URL>',
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[license_link]: https://opensource.org/licenses/MIT
|
||||
@@ -0,0 +1 @@
|
||||
include: package:very_good_analysis/analysis_options.4.0.0.yaml
|
||||
@@ -0,0 +1,14 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
source_gen|combining_builder:
|
||||
options:
|
||||
ignore_for_file:
|
||||
- implicit_dynamic_parameter
|
||||
- require_trailing_commas
|
||||
- cast_nullable_to_non_nullable
|
||||
- lines_longer_than_80_chars
|
||||
json_serializable:
|
||||
options:
|
||||
field_rename: snake
|
||||
checked: true
|
||||
@@ -0,0 +1,12 @@
|
||||
// ignore_for_file: avoid_print
|
||||
import 'package:jwt/jwt.dart' as jwt;
|
||||
|
||||
Future<void> main() async {
|
||||
final token = await jwt.verify(
|
||||
'<TOKEN>',
|
||||
issuer: '<ISSUER>',
|
||||
audience: '<AUDIENCE>',
|
||||
publicKeysUrl: '<PUBLIC_KEYS_URL>',
|
||||
);
|
||||
print(token);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'src/jwt.dart';
|
||||
export 'src/models/models.dart';
|
||||
@@ -0,0 +1,219 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:jwt/jwt.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:pointycastle/pointycastle.dart';
|
||||
import 'package:rsa_pkcs/rsa_pkcs.dart' as rsa;
|
||||
|
||||
/// {@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});
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
final get = getOverride ?? http.get;
|
||||
final response = await get(Uri.parse(url));
|
||||
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw const JwtVerificationFailure('Could not fetch public keys.');
|
||||
}
|
||||
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)),
|
||||
);
|
||||
|
||||
return publicKeys;
|
||||
}
|
||||
|
||||
/// Typedef for a function that returns the public keys asynchronously.
|
||||
typedef GetPublicKeys = Future<Map<String, String>> Function();
|
||||
|
||||
/// {@template jwt_verification_failure}
|
||||
/// An exception thrown during JWT verification.
|
||||
/// {@endtemplate}
|
||||
class JwtVerificationFailure implements Exception {
|
||||
/// {@macro jwt_verification_failure}
|
||||
const JwtVerificationFailure(this.reason);
|
||||
|
||||
/// The reason for the verification failure.
|
||||
final String reason;
|
||||
|
||||
@override
|
||||
String toString() => 'JwtVerificationFailure: $reason';
|
||||
}
|
||||
|
||||
/// Verify the provided [jwt].
|
||||
Future<Jwt> verify(
|
||||
String jwt, {
|
||||
required String issuer,
|
||||
required String audience,
|
||||
required String publicKeysUrl,
|
||||
}) async {
|
||||
final parts = jwt.split('.');
|
||||
|
||||
if (parts.length != 3) {
|
||||
throw const JwtVerificationFailure('JWT is malformed');
|
||||
}
|
||||
|
||||
final publicKeys = await _getPublicKeys(publicKeysUrl);
|
||||
|
||||
final JwtHeader header;
|
||||
try {
|
||||
header = JwtHeader.fromJson(_decodePart(parts[0]));
|
||||
} catch (_) {
|
||||
throw const JwtVerificationFailure('JWT header is malformed.');
|
||||
}
|
||||
await _verifyHeader(header, publicKeys);
|
||||
|
||||
final JwtPayload payload;
|
||||
try {
|
||||
payload = JwtPayload.fromJson(_decodePart(parts[1]));
|
||||
} catch (_) {
|
||||
throw const JwtVerificationFailure('JWT payload is malformed.');
|
||||
}
|
||||
_verifyPayload(payload, issuer, audience);
|
||||
|
||||
final isValid = _verifySignature(jwt, publicKeys[header.kid]!);
|
||||
if (!isValid) {
|
||||
throw const JwtVerificationFailure('Invalid signature.');
|
||||
}
|
||||
|
||||
return Jwt(
|
||||
header: header,
|
||||
payload: payload,
|
||||
signature: parts[2],
|
||||
claims: _decodePart(parts[1]),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _decodePart(String part) {
|
||||
final normalized = base64.normalize(part);
|
||||
final base64Decoded = base64.decode(normalized);
|
||||
final utf8Decoded = utf8.decode(base64Decoded);
|
||||
final jsonDecoded = json.decode(utf8Decoded) as Map<String, dynamic>;
|
||||
return jsonDecoded;
|
||||
}
|
||||
|
||||
Future<void> _verifyHeader(
|
||||
JwtHeader header,
|
||||
Map<String, dynamic> publicKeys,
|
||||
) async {
|
||||
if (header.typ != 'JWT') {
|
||||
throw const JwtVerificationFailure('Invalid token type.');
|
||||
}
|
||||
|
||||
if (header.alg != 'RS256') {
|
||||
throw const JwtVerificationFailure('Invalid algorithm.');
|
||||
}
|
||||
|
||||
if (!publicKeys.containsKey(header.kid)) {
|
||||
throw const JwtVerificationFailure('Invalid key id.');
|
||||
}
|
||||
}
|
||||
|
||||
void _verifyPayload(JwtPayload payload, String issuer, String audience) {
|
||||
final now = clock.now();
|
||||
|
||||
final exp = DateTime.fromMillisecondsSinceEpoch(payload.exp * 1000);
|
||||
if (exp.isBefore(now)) {
|
||||
throw const JwtVerificationFailure('Token has expired.');
|
||||
}
|
||||
|
||||
final iat = DateTime.fromMillisecondsSinceEpoch(payload.iat * 1000);
|
||||
if (iat.isAfter(now)) {
|
||||
throw const JwtVerificationFailure('Token issued at a future time.');
|
||||
}
|
||||
|
||||
final authTime = DateTime.fromMillisecondsSinceEpoch(payload.authTime * 1000);
|
||||
if (authTime.isAfter(now)) {
|
||||
throw const JwtVerificationFailure('Authenticated at a future time.');
|
||||
}
|
||||
|
||||
if (payload.aud != audience) {
|
||||
throw const JwtVerificationFailure('Invalid audience.');
|
||||
}
|
||||
|
||||
if (payload.iss != issuer) {
|
||||
throw const JwtVerificationFailure('Invalid issuer.');
|
||||
}
|
||||
|
||||
if (payload.sub.isEmpty) {
|
||||
throw const JwtVerificationFailure('Invalid subject.');
|
||||
}
|
||||
}
|
||||
|
||||
bool _verifySignature(String jwt, String publicKey) {
|
||||
final parts = jwt.split('.');
|
||||
final encodedHeader = parts[0];
|
||||
final encodedPayload = parts[1];
|
||||
final signature = parts[2];
|
||||
final body = utf8.encode('$encodedHeader.$encodedPayload');
|
||||
final sign = base64Url.decode(base64Padded(signature));
|
||||
|
||||
final parser = rsa.RSAPKCSParser();
|
||||
final pair = parser.parsePEM(publicKey);
|
||||
if (pair.public is! rsa.RSAPublicKey) return false;
|
||||
final public = pair.public;
|
||||
|
||||
try {
|
||||
final signer = Signer('SHA-256/RSA');
|
||||
final key = RSAPublicKey(
|
||||
public!.modulus,
|
||||
BigInt.from(public.publicExponent),
|
||||
);
|
||||
final param = ParametersWithRandom(
|
||||
PublicKeyParameter<RSAPublicKey>(key),
|
||||
SecureRandom('AES/CTR/PRNG'),
|
||||
);
|
||||
signer.init(false, param);
|
||||
final rsaSignature = RSASignature(Uint8List.fromList(sign));
|
||||
return signer.verifySignature(Uint8List.fromList(body), rsaSignature);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Visible for testing only
|
||||
@visibleForTesting
|
||||
String base64Padded(String value) {
|
||||
final mod = value.length % 4;
|
||||
if (mod == 0) {
|
||||
return value;
|
||||
} else if (mod == 3) {
|
||||
return value.padRight(value.length + 1, '=');
|
||||
} else if (mod == 2) {
|
||||
return value.padRight(value.length + 2, '=');
|
||||
} else {
|
||||
return value; // let it fail when decoding
|
||||
}
|
||||
}
|
||||
|
||||
/// Override for http.get.
|
||||
/// Used for testing purposes only.
|
||||
@visibleForTesting
|
||||
Future<http.Response> Function(Uri uri)? getOverride;
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:jwt/jwt.dart';
|
||||
|
||||
/// {@template jwt}
|
||||
/// A JWT (json web token)
|
||||
/// {@endtemplate}
|
||||
class Jwt {
|
||||
/// {@macro jwt}
|
||||
const Jwt({
|
||||
required this.header,
|
||||
required this.payload,
|
||||
required this.signature,
|
||||
this.claims = const <String, dynamic>{},
|
||||
});
|
||||
|
||||
/// {@macro jwt_header}
|
||||
final JwtHeader header;
|
||||
|
||||
/// {@macro jwt_payload}
|
||||
final JwtPayload payload;
|
||||
|
||||
/// JWT signature.
|
||||
final String signature;
|
||||
|
||||
/// Token claims.
|
||||
final Map<String, dynamic> claims;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'jwt_header.g.dart';
|
||||
|
||||
/// {@template jwt_header}
|
||||
/// A JWT header which contains the algorithm and token type.
|
||||
/// {@endtemplate}
|
||||
@JsonSerializable(createToJson: false)
|
||||
class JwtHeader {
|
||||
/// {@macro jwt_header}
|
||||
const JwtHeader({
|
||||
required this.alg,
|
||||
required this.kid,
|
||||
required this.typ,
|
||||
});
|
||||
|
||||
/// Decode a [JwtHeader] from a `Map<String, dynamic>`.
|
||||
factory JwtHeader.fromJson(Map<String, dynamic> json) {
|
||||
return _$JwtHeaderFromJson(json);
|
||||
}
|
||||
|
||||
/// Signature or encryption algorithm.
|
||||
final String alg;
|
||||
|
||||
/// Key ID.
|
||||
final String kid;
|
||||
|
||||
/// Type of token.
|
||||
final String typ;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 'jwt_header.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
JwtHeader _$JwtHeaderFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'JwtHeader',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = JwtHeader(
|
||||
alg: $checkedConvert('alg', (v) => v as String),
|
||||
kid: $checkedConvert('kid', (v) => v as String),
|
||||
typ: $checkedConvert('typ', (v) => v as String),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'jwt_payload.g.dart';
|
||||
|
||||
/// {@template jwt_payload}
|
||||
/// A JWT payload which contains data.
|
||||
/// {@endtemplate}
|
||||
@JsonSerializable(createToJson: false)
|
||||
class JwtPayload {
|
||||
/// {@macro jwt_payload}
|
||||
JwtPayload({
|
||||
required this.exp,
|
||||
required this.iat,
|
||||
required this.aud,
|
||||
required this.iss,
|
||||
required this.sub,
|
||||
required this.authTime,
|
||||
});
|
||||
|
||||
/// Decode a [JwtPayload] from a `Map<String, dynamic>`.
|
||||
factory JwtPayload.fromJson(Map<String, dynamic> json) {
|
||||
return _$JwtPayloadFromJson(json);
|
||||
}
|
||||
|
||||
/// Expiration time (seconds since Unix epoch).
|
||||
final int exp;
|
||||
|
||||
/// Issued at (seconds since Unix epoch).
|
||||
final int iat;
|
||||
|
||||
/// Audience (who or what the token is intended for).
|
||||
final String aud;
|
||||
|
||||
/// Issuer (who created and signed this token).
|
||||
final String iss;
|
||||
|
||||
/// Subject (whom the token refers to).
|
||||
final String sub;
|
||||
|
||||
/// Time when authentication occurred.
|
||||
final int authTime;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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 'jwt_payload.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
JwtPayload _$JwtPayloadFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'JwtPayload',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = JwtPayload(
|
||||
exp: $checkedConvert('exp', (v) => v as int),
|
||||
iat: $checkedConvert('iat', (v) => v as int),
|
||||
aud: $checkedConvert('aud', (v) => v as String),
|
||||
iss: $checkedConvert('iss', (v) => v as String),
|
||||
sub: $checkedConvert('sub', (v) => v as String),
|
||||
authTime: $checkedConvert('auth_time', (v) => v as int),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
fieldKeyMap: const {'authTime': 'auth_time'},
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'jwt.dart';
|
||||
export 'jwt_header.dart';
|
||||
export 'jwt_payload.dart';
|
||||
@@ -0,0 +1,21 @@
|
||||
name: jwt
|
||||
description: A Dart Json Web Token Library
|
||||
version: 1.0.0+1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
clock: ^1.1.0
|
||||
http: ^0.13.4
|
||||
json_annotation: ^4.4.0
|
||||
meta: ^1.7.0
|
||||
pointycastle: ^3.5.0
|
||||
rsa_pkcs: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.0.0
|
||||
json_serializable: ^6.1.4
|
||||
test: ^1.19.2
|
||||
very_good_analysis: ^4.0.0
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-----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-----
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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:test/test.dart';
|
||||
|
||||
void main() {
|
||||
const token =
|
||||
'''eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxMGM5MGJhNGMzNjYzNTE2ZTA3MDdkMGU5YTg5NDgxMDYyODUxNTgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwiYXV0aF90aW1lIjoxNjQzNjg0MjY2LCJ1c2VyX2lkIjoiRzR1MzdXdk90dmVWR0pRb1pCWGpxcHVWazZWMiIsInN1YiI6Ikc0dTM3V3ZPdHZlVkdKUW9aQlhqcXB1Vms2VjIiLCJpYXQiOjE2NDM2ODQyNjYsImV4cCI6MTY0MzY4Nzg2NiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3RAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.bUWnX_XmR1d9EmeFeYSsK_CHU1u9NPIHgyaQueZ6urYOtxvuL_QodjPl0c9CBJwctwPnxVyRmkeNCw0oF9xBgph0NApLL4FIG6vpDPZfW9txZBYr8xIvaqvmD0diACENAQdjRT2XmyEdQ2-U7SsTonybHmLoU9FMQTjAgw4NCALQvExfB6rtQ9GDsOBt1xoBkB3Vo7a5OmugZ1aHXF69b8As6137-Dggf5qx5R3oLRFovICMMesQziE3vGi-WKcbQxSeiD-9a6ShPAhk41XiyjFGDEOtUCQo63uwQnMw3g0KVtC6bzIyFq-E91vhxumxXzxPYC-kg7iUYiSZy7Y-Aw''';
|
||||
const publicKeysUrl =
|
||||
'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},
|
||||
);
|
||||
final expiresAt = DateTime.fromMillisecondsSinceEpoch(1643687866 * 1000);
|
||||
final validTime = expiresAt.subtract(Duration(minutes: 15));
|
||||
|
||||
group('verify', () {
|
||||
test('can be instantiated', () {
|
||||
expect(verify, isNotNull);
|
||||
});
|
||||
|
||||
test('can verify an expired jwt', () async {
|
||||
getOverride = (Uri uri) async {
|
||||
return Response(
|
||||
body,
|
||||
HttpStatus.ok,
|
||||
headers: {'cache-control': 'max-age=3600'},
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
await verify(
|
||||
token,
|
||||
audience: audience,
|
||||
issuer: issuer,
|
||||
publicKeysUrl: publicKeysUrl,
|
||||
);
|
||||
fail('should throw');
|
||||
} catch (error) {
|
||||
expect(
|
||||
error,
|
||||
isA<JwtVerificationFailure>().having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
'Token has expired.',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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.',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
token,
|
||||
audience: audience,
|
||||
issuer: 'https://invalid/issuer',
|
||||
publicKeysUrl: publicKeysUrl,
|
||||
);
|
||||
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 {
|
||||
getOverride = (Uri uri) async {
|
||||
return Response(
|
||||
body,
|
||||
HttpStatus.ok,
|
||||
headers: {'cache-control': 'max-age=3600'},
|
||||
);
|
||||
};
|
||||
final jwt = await verify(
|
||||
token,
|
||||
audience: audience,
|
||||
issuer: issuer,
|
||||
publicKeysUrl: publicKeysUrl,
|
||||
);
|
||||
expect(jwt, isA<Jwt>());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('base64Padded', () {
|
||||
test('does not add padding when mod 4 == 0', () {
|
||||
const value = 'aaaa';
|
||||
expect(base64Padded(value), equals(value));
|
||||
});
|
||||
|
||||
test('does not add padding when mod 4 == 1', () {
|
||||
const value = 'aaaaa';
|
||||
expect(base64Padded(value), equals(value));
|
||||
});
|
||||
|
||||
test('adds padding when mod 4 == 3', () {
|
||||
const value = 'aaaaaaa';
|
||||
expect(base64Padded(value), equals('$value='));
|
||||
});
|
||||
|
||||
test('adds padding when mod 4 == 2', () {
|
||||
const value = 'aaaaaa';
|
||||
expect(base64Padded(value), equals('$value=='));
|
||||
});
|
||||
});
|
||||
|
||||
group('JwtVerificationFailure', () {
|
||||
test('toString is correct', () {
|
||||
const reason = 'reason';
|
||||
final failure = JwtVerificationFailure(reason);
|
||||
expect(failure.toString(), equals('JwtVerificationFailure: $reason'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user