Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies:
git:
url: https://github.com/invertase/dart_firebase_admin.git
path: packages/dart_firebase_admin
ref: next
ref: main
shelf: ^1.4.2
shelf_router: ^1.1.2

Expand Down
56 changes: 36 additions & 20 deletions packages/dart_firebase_admin/lib/src/utils/jwt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:convert';

import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:http/http.dart' as http;
import 'package:jose/jose.dart';
import 'package:meta/meta.dart';

const algorithmRS256 = 'RS256';
Expand Down Expand Up @@ -41,19 +40,19 @@ abstract class SignatureVerifier {
}

abstract class KeyFetcher {
Future<JsonWebKeyStore> fetchPublicKeys();
Future<Map<String, JWTKey>> fetchPublicKeys();
}

class UrlKeyFetcher implements KeyFetcher {
UrlKeyFetcher(this.clientCert);

final Uri clientCert;

JsonWebKeyStore? _publicKeys;
Map<String, JWTKey>? _publicKeys;
late DateTime _publicKeysExpireAt;

@override
Future<JsonWebKeyStore> fetchPublicKeys() async {
Future<Map<String, JWTKey>> fetchPublicKeys() async {
if (_shouldRefresh()) return refresh();
return _publicKeys!;
}
Expand All @@ -63,7 +62,7 @@ class UrlKeyFetcher implements KeyFetcher {
return _publicKeysExpireAt.isBefore(DateTime.now());
}

Future<JsonWebKeyStore> refresh() async {
Future<Map<String, JWTKey>> refresh() async {
final response = await http.get(clientCert);
final json = jsonDecode(response.body) as Map<String, Object?>;
final error = json['error'];
Expand Down Expand Up @@ -91,26 +90,27 @@ class UrlKeyFetcher implements KeyFetcher {
}
}

final store = _publicKeys = JsonWebKeyStore();
final keys = <String, JWTKey>{};

for (final entry in json.entries) {
final key = JsonWebKey.fromPem(entry.value! as String, keyId: entry.key);
store.addKey(key);
final pem = entry.value! as String;
// Google X.509 certs are RSA
keys[entry.key] = RSAPublicKey.cert(pem);
}

return store;
return _publicKeys = keys;
}
}

class JwksFetcher implements KeyFetcher {
JwksFetcher(this.jwksUrl);
final Uri jwksUrl;
JsonWebKeyStore? _publicKeys;
Map<String, JWTKey>? _publicKeys;
int _publicKeysExpireAt = 0;
static const int hourInMilliseconds = 6 * 60 * 60 * 1000; // 6 hours

@override
Future<JsonWebKeyStore> fetchPublicKeys() async {
Future<Map<String, JWTKey>> fetchPublicKeys() async {
if (_shouldRefresh) return refresh();

return _publicKeys!;
Expand All @@ -121,27 +121,36 @@ class JwksFetcher implements KeyFetcher {
_publicKeysExpireAt <= DateTime.now().millisecondsSinceEpoch;
}

Future<JsonWebKeyStore> refresh() async {
Future<Map<String, JWTKey>> refresh() async {
final response = await http.get(jwksUrl);
if (response.statusCode != 200) {
throw Exception('Failed to fetch JWKS');
}

final jwks = jsonDecode(response.body) as Map<String, dynamic>;
final keys = JsonWebKeySet.fromJson(jwks).keys;
final jwkList = jwks['keys'] as List<dynamic>?;

if (jwkList == null) {
throw Exception('Invalid JWKS: missing "keys" array');
}

// Reset expire time
_publicKeysExpireAt = 0;

// Extract signing keys
final store = _publicKeys = JsonWebKeyStore();
keys.forEach(store.addKey);
final newKeys = <String, JWTKey>{};
for (final jwkJson in jwkList) {
final jwk = jwkJson as Map<String, dynamic>;
final kid = jwk['kid'] as String?;
if (kid != null) {
newKeys[kid] = JWTKey.fromJWK(jwk);
}
}

// Set new expiration time
_publicKeysExpireAt =
DateTime.now().millisecondsSinceEpoch + hourInMilliseconds;

return store;
return _publicKeys = newKeys;
}
}

Expand Down Expand Up @@ -175,10 +184,15 @@ class PublicKeySignatureVerifier implements SignatureVerifier {
);
}

final store = await keyFetcher.fetchPublicKeys();
final keys = await keyFetcher.fetchPublicKeys();
final key = keys[kid];

if (key == null) {
throw JwtException(JwtErrorCode.noMatchingKid, 'no-matching-kid-error');
}

try {
await JsonWebToken.decodeAndVerify(token, store);
JWT.verify(token, key);
} catch (e, stackTrace) {
Error.throwWithStackTrace(
JwtException(
Expand All @@ -194,7 +208,9 @@ class PublicKeySignatureVerifier implements SignatureVerifier {
} on JWTException catch (e) {
throw JwtException(
JwtErrorCode.unknown,
e is JWTUndefinedException ? e.message : '${e.runtimeType}: e.message',
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There a bug that got fixed, too! (e.message) was not being evaluated – just a string.

e is JWTUndefinedException
? e.message
: '${e.runtimeType}: ${e.message}',
);
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/dart_firebase_admin/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ dependencies:
googleapis_beta: ^9.0.0
http: ^1.0.0
intl: ^0.20.0
jose: ^0.3.4
meta: ^1.9.1
pem: ^2.0.5
pointycastle: ^3.7.0
Expand Down
16 changes: 7 additions & 9 deletions packages/dart_firebase_admin/test/unit/auth/jwt_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:dart_firebase_admin/src/utils/jwt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:jose/jose.dart';
import 'package:pointycastle/pointycastle.dart' as pc;
import 'package:test/test.dart';

import '../../fixtures/mock_service_account.dart';
Expand Down Expand Up @@ -231,14 +231,12 @@ void main() {

class _TestKeyFetcher implements KeyFetcher {
@override
Future<JsonWebKeyStore> fetchPublicKeys() async {
final store = JsonWebKeyStore();

// Public key corresponding to the test private key above
const key = mockPrivateKey;

store.addKey(JsonWebKey.fromPem(key, keyId: 'key1'));
Future<Map<String, JWTKey>> fetchPublicKeys() async {
final privateKey = RSAPrivateKey(mockPrivateKey);
final publicKey = RSAPublicKey.raw(
pc.RSAPublicKey(privateKey.key.n!, privateKey.key.publicExponent!),
);

return store;
return {'key1': publicKey};
}
}
Loading