Skip to content

Commit

Permalink
Fix Auth.verifyIdToken() (#54)
Browse files Browse the repository at this point in the history
Finish implementing the token verification using the updated dart_jsonwebtoken library
  • Loading branch information
labrom authored Oct 10, 2024
1 parent 8935be5 commit 67a03c7
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 8 deletions.
5 changes: 5 additions & 0 deletions packages/dart_firebase_admin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.4.1

- Fixes the `Auth.verifyIdToken()` implementation by adding the
token signature verification part.

## 0.4.0 - 2024-09-11

- Added `firestore.listCollections()` and `doc.listCollections()`
Expand Down
51 changes: 44 additions & 7 deletions packages/dart_firebase_admin/lib/src/utils/jwt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class EmulatorSignatureVerifier implements SignatureVerifier {
Future<void> verify(String token) async {
// Signature checks skipped for emulator; no need to fetch public keys.
try {
return await verifyJwtSignature(
verifyJwtSignature(
token,
SecretKey(''),
);
Expand Down Expand Up @@ -95,25 +95,61 @@ class PublicKeySignatureVerifier implements SignatureVerifier {

final KeyFetcher keyFetcher;

/// Verifies a JWT token.
///
/// This verifies the token's signature. The signing key is selected using the
/// 'kid' claim in the token's header.
/// The token's expiration is also verified.
@override
Future<bool> verify(String token) {
throw UnimplementedError();
// verifyJwtSignature(token);
Future<void> verify(String token) async {
try {
final jwt = JWT.decode(token);
final kid = jwt.header?['kid'] as String?;

if (kid == null) {
throw JwtError(
JwtErrorCode.noKidInHeader,
'no-kid-in-header-error',
);
}

final publicKeys = await keyFetcher.fetchPublicKeys();
final publicKey = publicKeys[kid];

if (publicKey == null) {
throw JwtError(
JwtErrorCode.noMatchingKid,
'no-matching-kid-error',
);
}
verifyJwtSignature(
token,
RSAPublicKey.cert(publicKey),
issueAt: Duration.zero, // Any past date should be valid
);
// At this point most JWTException's should have been caught in
// verifyJwtSignature, but we could still get some from JWT.decode above
} on JWTException catch (e) {
throw JwtError(
JwtErrorCode.unknown,
e is JWTUndefinedException ? e.message : '${e.runtimeType}: e.message',
);
}
}
}

sealed class SecretOrPublicKey {}

@internal
Future<void> verifyJwtSignature(
void verifyJwtSignature(
String token,
JWTKey key, {
Duration? issueAt,
Audience? audience,
String? subject,
String? issuer,
String? jwtId,
}) async {
}) {
try {
JWT.verify(
token,
Expand Down Expand Up @@ -160,7 +196,8 @@ enum JwtErrorCode {
invalidSignature('invalid-token'),
noMatchingKid('no-matching-kid-error'),
noKidInHeader('no-kid-error'),
keyFetchError('key-fetch-error');
keyFetchError('key-fetch-error'),
unknown('unknown');

const JwtErrorCode(this.value);

Expand Down
3 changes: 2 additions & 1 deletion packages/dart_firebase_admin/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ environment:
dependencies:
collection: ^1.18.0
crypto: ^3.0.3
dart_jsonwebtoken: ^2.11.0
dart_jsonwebtoken: ^2.14.1
firebaseapis: ^0.2.0
freezed_annotation: ^2.4.1
googleapis_auth: ^1.3.0
Expand All @@ -19,6 +19,7 @@ dependencies:
meta: ^1.9.1

dev_dependencies:
asn1lib: ^1.5.6
build_runner: ^2.4.7
file: ^7.0.0
freezed: ^2.4.2
Expand Down
118 changes: 118 additions & 0 deletions packages/dart_firebase_admin/test/auth/jwt_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'dart:convert';

import 'package:asn1lib/asn1lib.dart';
import 'package:dart_firebase_admin/src/utils/jwt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:test/test.dart';

void main() {
group('PublicKeySignatureVerifier', () {
final privateKey = RSAPrivateKey('''
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
dn/RsYEONbwQSjIfMPkvxF+8HQ==
-----END PRIVATE KEY-----
''');
final keyFetcher = _TestKeyFetcher();
final payload = {
'a': '1',
'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
};

test('valid kid should pass', () async {
final jwt = JWT(
payload,
header: {'kid': 'key1'},
);
final token = jwt.sign(
privateKey,
algorithm: JWTAlgorithm.RS256,
);
await PublicKeySignatureVerifier(keyFetcher).verify(token);
});
test('no kid should throw', () async {
final jwt = JWT(payload);
final token = jwt.sign(
privateKey,
algorithm: JWTAlgorithm.RS256,
);
await expectLater(
PublicKeySignatureVerifier(keyFetcher).verify(token),
throwsA(isA<JwtError>()),
);
});
test('invalid kid should throw', () async {
final jwt = JWT(
payload,
header: {'kid': 'key2'},
);
final token = jwt.sign(
privateKey,
algorithm: JWTAlgorithm.RS256,
);
await expectLater(
PublicKeySignatureVerifier(keyFetcher).verify(token),
throwsA(isA<JwtError>()),
);
});
});
}

class _TestKeyFetcher implements KeyFetcher {
@override
Future<Map<String, String>> fetchPublicKeys() {
return Future.value({
'key1': _publicKeyAsCert('''
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
'''),
});
}
}

String _publicKeyAsCert(String publicKey) {
final lines = LineSplitter.split(publicKey)
.map((line) => line.trim())
.where((line) => line.isNotEmpty)
.toList();
final base64 = lines.sublist(1, lines.length - 1).join();
final bytes = base64Decode(base64);
final certSequence = ASN1Sequence();
final root = ASN1Sequence()..add(certSequence);
for (var i = 0; i < 5; i++) {
certSequence.add(ASN1Integer.fromInt(0));
}
certSequence.add(ASN1Object.fromBytes(bytes));
return '${lines.first}\n${base64Encode(root.encodedBytes)}\n${lines.last}';
}

0 comments on commit 67a03c7

Please sign in to comment.