Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
78 changes: 45 additions & 33 deletions lib/src/identity/token_verifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ library;

import 'dart:convert';

import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:http/http.dart' as http;
import 'package:jose/jose.dart'
show JoseException, JsonWebKey, JsonWebKeyStore, JsonWebToken;

import '../https/error.dart';

Expand Down Expand Up @@ -38,8 +37,8 @@ class AuthBlockingTokenVerifier {
final bool isEmulator;
final http.Client _httpClient;

/// Cached JsonWebKeyStore with Google's public keys.
static JsonWebKeyStore? _cachedKeyStore;
/// Cached public keys.
static Map<String, JWTKey>? _cachedKeys;

/// When the cached keys expire.
static DateTime? _keysExpireAt;
Expand All @@ -65,21 +64,38 @@ class AuthBlockingTokenVerifier {
return _unsafeDecode(token);
}

// Get the key store with Google's keys
final keyStore = await _getGoogleKeyStore();
// Decode the token first to get the header
JWT decoded;
try {
decoded = JWT.decode(token);
} catch (e) {
throw UnauthenticatedError('Invalid JWT format: $e');
}

// Verify the token using jose
JsonWebToken jwt;
final kid = decoded.header?['kid'] as String?;
if (kid == null) {
throw UnauthenticatedError('Invalid JWT: missing "kid" header');
}

// Get the keys from Google
final keys = await _getGoogleKeys();
final key = keys[kid];

if (key == null) {
throw UnauthenticatedError('Invalid JWT: unknown "kid"');
}

// Verify the token using dart_jsonwebtoken
try {
jwt = await JsonWebToken.decodeAndVerify(token, keyStore);
} on JoseException catch (e) {
JWT.verify(token, key);
} on JWTException catch (e) {
throw UnauthenticatedError('Invalid JWT: ${e.message}');
} catch (e) {
throw UnauthenticatedError('Invalid JWT: $e');
}

// Extract the payload as a map
final payload = jwt.claims.toJson();
final payload = decoded.payload as Map<String, dynamic>;

// Validate Firebase-specific claims
_validateClaims(payload, audience);
Expand All @@ -89,31 +105,24 @@ class AuthBlockingTokenVerifier {

/// Decodes a JWT without verification (for emulator mode only).
Map<String, dynamic> _unsafeDecode(String token) {
final parts = token.split('.');
if (parts.length != 3) {
try {
final decoded = JWT.decode(token);
return decoded.payload as Map<String, dynamic>;
} catch (e) {
throw InvalidArgumentError('Invalid JWT format');
}

final payloadJson = _decodeBase64Url(parts[1]);
return jsonDecode(payloadJson) as Map<String, dynamic>;
}

/// Decodes a base64url string to a UTF-8 string.
String _decodeBase64Url(String input) {
final normalized = base64Url.normalize(input);
return utf8.decode(base64Url.decode(normalized));
}

/// Fetches Google's public keys and creates a JsonWebKeyStore.
/// Fetches Google's public keys and returns a map of kid to JWTKey.
///
/// Uses the JWK endpoint which returns keys directly in JSON Web Key format,
/// so no manual certificate parsing is needed.
Future<JsonWebKeyStore> _getGoogleKeyStore() async {
// Return cached key store if still valid
if (_cachedKeyStore != null &&
Future<Map<String, JWTKey>> _getGoogleKeys() async {
// Return cached keys if still valid
if (_cachedKeys != null &&
_keysExpireAt != null &&
DateTime.now().isBefore(_keysExpireAt!)) {
return _cachedKeyStore!;
return _cachedKeys!;
}

// Fetch keys from Google's JWK endpoint
Expand All @@ -127,15 +136,18 @@ class AuthBlockingTokenVerifier {

// Parse the JWK Set response
final jwksJson = jsonDecode(response.body) as Map<String, dynamic>;
final keyStore = JsonWebKeyStore();
final newKeys = <String, JWTKey>{};

// The response contains a "keys" array with JWK objects
final keys = jwksJson['keys'] as List<dynamic>?;
if (keys != null) {
for (final keyJson in keys) {
try {
final jwk = JsonWebKey.fromJson(keyJson as Map<String, dynamic>);
keyStore.addKey(jwk);
final jwk = keyJson as Map<String, dynamic>;
final kid = jwk['kid'] as String?;
if (kid != null) {
newKeys[kid] = JWTKey.fromJWK(jwk);
}
} catch (e) {
// Skip keys that fail to parse
continue;
Expand All @@ -154,10 +166,10 @@ class AuthBlockingTokenVerifier {
}
}

_cachedKeyStore = keyStore;
_cachedKeys = newKeys;
_keysExpireAt = DateTime.now().add(cacheDuration);

return keyStore;
return newKeys;
}

/// Validates JWT claims.
Expand Down Expand Up @@ -228,7 +240,7 @@ class AuthBlockingTokenVerifier {

/// Clears the key cache (useful for testing).
static void clearCertificateCache() {
_cachedKeyStore = null;
_cachedKeys = null;
_keysExpireAt = null;
}
}
4 changes: 1 addition & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,14 @@ dependencies:
stack_trace: ^1.12.1
protobuf: ^6.0.0
http: ^1.6.0
# Pin as this is an external dependency.
# TODO: Vendor in this package.
jose: 0.3.5
# Required for builder
build: ^4.0.4
source_gen: ^4.2.0
analyzer: ^10.0.1
glob: ^2.1.3
yaml_edit: ^2.2.3
google_cloud_storage: ^0.5.1
dart_jsonwebtoken: ^3.1.1

dev_dependencies:
build_runner: ^2.10.5
Expand Down
Loading