Add more checks to JWTs
diff --git a/app_dart/lib/src/service/firebase_jwt_validator.dart b/app_dart/lib/src/service/firebase_jwt_validator.dart index ad5f0b5..6f4db44 100644 --- a/app_dart/lib/src/service/firebase_jwt_validator.dart +++ b/app_dart/lib/src/service/firebase_jwt_validator.dart
@@ -59,7 +59,14 @@ final JsonWebToken jwt; try { - jwt = await JsonWebToken.decodeAndVerify(jwtString, _keyStore); + jwt = await JsonWebToken.decodeAndVerify( + jwtString, + _keyStore, + allowedArguments: [ + // Firebase Verify ID Tokens: https://firebase.google.com/docs/auth/admin/verify-id-tokens + 'RS256', + ], + ); } catch (e) { throw JwtException('Unable to decode and verify: $e'); } @@ -70,10 +77,28 @@ void _ensureNoPublicKeys(String jwtString) { final jwt = JoseObject.fromCompactSerialization(jwtString); final header = jwt.commonProtectedHeader; + // Firebase Verify ID Tokens: https://firebase.google.com/docs/auth/admin/verify-id-tokens + if (header['alg'] != 'RS256') { + throw JwtException('Bad algorithm'); + } if (header['jwk'] case final publicKey?) { throw JwtException('Public key in JWT not allowed: $publicKey'); } - + if (header['jku'] != null) { + throw JwtException('JWK Set URL not allowed'); + } + if (header['x5u'] != null) { + throw JwtException('x5u not allowed'); + } + if (header['x5c'] != null) { + throw JwtException('x5c not allowed'); + } + if (header['x5t'] != null) { + throw JwtException('x5t not allowed'); + } + if (header['x5t#S256'] != null) { + throw JwtException('x5t#S256 not allowed'); + } if (header['kid'] == null) { throw JwtException('Required `kid` field missing'); }
diff --git a/app_dart/test/service/firebase_jwt_validator_test.dart b/app_dart/test/service/firebase_jwt_validator_test.dart index 966b4ff..04868ff 100644 --- a/app_dart/test/service/firebase_jwt_validator_test.dart +++ b/app_dart/test/service/firebase_jwt_validator_test.dart
@@ -266,7 +266,24 @@ }); test('fails if JWT has a public key in header', () async { - final key = JsonWebKey.generate('RS256'); + // HMAC signing + final key = JsonWebKey.generate('HS256'); + final builder = JsonWebSignatureBuilder() + ..jsonContent = {'sub': '123'} + ..setProtectedHeader('jwk', key.toJson()) + ..setProtectedHeader('kid', 'some-kid') + ..addRecipient(key, algorithm: 'HS256'); + final jws = builder.build(); + final jwtString = jws.toCompactSerialization(); + + await expectLater( + validator.decodeAndVerify(jwtString), + jwtException('Bad algorithm'), + ); + }); + + test('fails if JWT algorithm is not firebase approved', () async { + final key = JsonWebKey.generate('RS257'); final builder = JsonWebSignatureBuilder() ..jsonContent = {'sub': '123'} ..setProtectedHeader('jwk', key.toJson()) @@ -281,6 +298,86 @@ ); }); + test('fails if JWT has jku in header', () async { + final key = JsonWebKey.generate('RS256'); + final builder = JsonWebSignatureBuilder() + ..jsonContent = {'sub': '123'} + ..setProtectedHeader('jku', 'https://example.com/keys.json') + ..setProtectedHeader('kid', 'some-kid') + ..addRecipient(key, algorithm: 'RS256'); + final jws = builder.build(); + final jwtString = jws.toCompactSerialization(); + + await expectLater( + validator.decodeAndVerify(jwtString), + jwtException('JWK Set URL not allowed'), + ); + }); + + test('fails if JWT has x5u in header', () async { + final key = JsonWebKey.generate('RS256'); + final builder = JsonWebSignatureBuilder() + ..jsonContent = {'sub': '123'} + ..setProtectedHeader('x5u', 'https://example.com/cert.pem') + ..setProtectedHeader('kid', 'some-kid') + ..addRecipient(key, algorithm: 'RS256'); + final jws = builder.build(); + final jwtString = jws.toCompactSerialization(); + + await expectLater( + validator.decodeAndVerify(jwtString), + jwtException('x5u not allowed'), + ); + }); + + test('fails if JWT has x5c in header', () async { + final key = JsonWebKey.generate('RS256'); + final builder = JsonWebSignatureBuilder() + ..jsonContent = {'sub': '123'} + ..setProtectedHeader('x5c', ['cert1', 'cert2']) + ..setProtectedHeader('kid', 'some-kid') + ..addRecipient(key, algorithm: 'RS256'); + final jws = builder.build(); + final jwtString = jws.toCompactSerialization(); + + await expectLater( + validator.decodeAndVerify(jwtString), + jwtException('x5c not allowed'), + ); + }); + + test('fails if JWT has x5t in header', () async { + final key = JsonWebKey.generate('RS256'); + final builder = JsonWebSignatureBuilder() + ..jsonContent = {'sub': '123'} + ..setProtectedHeader('x5t', 'thumbprint') + ..setProtectedHeader('kid', 'some-kid') + ..addRecipient(key, algorithm: 'RS256'); + final jws = builder.build(); + final jwtString = jws.toCompactSerialization(); + + await expectLater( + validator.decodeAndVerify(jwtString), + jwtException('x5t not allowed'), + ); + }); + + test('fails if JWT has x5t#S256 in header', () async { + final key = JsonWebKey.generate('RS256'); + final builder = JsonWebSignatureBuilder() + ..jsonContent = {'sub': '123'} + ..setProtectedHeader('x5t#S256', 'thumbprint256') + ..setProtectedHeader('kid', 'some-kid') + ..addRecipient(key, algorithm: 'RS256'); + final jws = builder.build(); + final jwtString = jws.toCompactSerialization(); + + await expectLater( + validator.decodeAndVerify(jwtString), + jwtException('x5t#S256 not allowed'), + ); + }); + test('fails if JWT is missing kid', () async { final key = JsonWebKey.generate('RS256'); final builder = JsonWebSignatureBuilder()