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
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
run: dart --version

- name: Install dependencies
run: dart pub get
run: dart pub upgrade

- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .
Expand Down Expand Up @@ -117,11 +117,11 @@ jobs:
sdk: stable

- name: Install dependencies (root)
run: dart pub get
run: dart pub upgrade

- name: Install dependencies (fixture)
working-directory: test/fixtures/dart_reference
run: dart pub get
run: dart pub upgrade

- name: Run build_runner
working-directory: test/fixtures/dart_reference
Expand Down Expand Up @@ -170,7 +170,7 @@ jobs:
cache-dependency-path: test/fixtures/nodejs_reference/package-lock.json

- name: Install Dart dependencies
run: dart pub get
run: dart pub upgrade

- name: Download Dart manifest from builder-tests
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -281,7 +281,7 @@ jobs:
run: firebase --version

- name: Install dependencies (root)
run: dart pub get
run: dart pub upgrade

- name: Download Dart manifest from builder-tests
uses: actions/download-artifact@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/validate-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ echo "========================================="
echo ""

echo "Installing dependencies..."
dart pub get
dart pub upgrade

echo "Checking formatting..."
if dart format --output=none --set-exit-if-changed .; then
Expand Down Expand Up @@ -104,7 +104,7 @@ echo ""

echo "Installing fixture dependencies..."
cd test/fixtures/dart_reference
dart pub get
dart pub upgrade

echo "Running build_runner..."
if dart run build_runner build --delete-conflicting-outputs; then
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ All submissions, including submissions by project members, require review. We us

```bash
cd firebase-functions-dart
dart pub get
dart pub upgrade
```

3. Verify your setup by running the analyzer and tests:
Expand Down
2 changes: 1 addition & 1 deletion example/client_app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A simple web client demonstrating how to call Firebase Functions (both `onReques

```bash
cd ../basic
dart pub get
dart pub upgrade
dart run build_runner build --delete-conflicting-outputs
firebase emulators:start --only functions,auth
```
Expand Down
80 changes: 47 additions & 33 deletions lib/src/identity/token_verifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,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 @@ -48,8 +47,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 @@ -75,21 +74,40 @@ 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');
}

final kid = decoded.header?['kid'];
if (kid is! String) {
throw UnauthenticatedError(
'Invalid JWT: missing "kid" header or not String',
);
}

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

// Verify the token using jose
JsonWebToken jwt;
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 @@ -99,31 +117,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 @@ -137,15 +148,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 @@ -164,10 +178,10 @@ class AuthBlockingTokenVerifier {
}
}

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

return keyStore;
return newKeys;
}

/// Validates JWT claims.
Expand Down Expand Up @@ -238,7 +252,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 @@ -53,16 +53,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
2 changes: 1 addition & 1 deletion test/fixtures/dart_reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This example demonstrates the core functionality of Firebase Functions for Dart.
### Install Dependencies

```bash
dart pub get
dart pub upgrade
```

### Run Locally
Expand Down
2 changes: 1 addition & 1 deletion test/snapshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Add to CI pipeline:
- name: Run Snapshot Tests
run: |
cd firebase-functions-dart
dart pub get
dart pub upgrade
dart run build_runner build
dart test test/snapshots/
```
Expand Down
Loading