Last active
March 13, 2022 15:44
-
-
Save starkdmi/e9be666330e6141c03ec7734eaafcd8b to your computer and use it in GitHub Desktop.
Verify Firebase App Check token
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:collection/collection.dart' show ListEquality; | |
import 'package:jose/jose.dart'; // BSD-3-Clause, use `dart_jsonwebtoken` package in case of MIT license | |
import 'package:dio/dio.dart'; // default `http` package can be used as well | |
const firebaseProjectId = "FIREBASE_PROJECT_ID"; | |
const firebaseProjectNumber = "FIREBASE_PROJECT_NUMBER"; | |
const firebaseAppIds = [ | |
"FIREBASE_IOS_APP_ID", | |
"FIREBASE_ANDROID_APP_ID", | |
"FIREBASE_WEB_APP_ID" | |
]; | |
// Verify App Check Token | |
// https://firebase.googleblog.com/2021/10/protecting-backends-with-app-check.html | |
Future<bool> appCheck(String token) async { | |
// Obtain the App Check public JSON Web Key Set | |
final keys = await fetchAppCheckKeys(); | |
if (keys.isEmpty) return false; | |
// Decode the token | |
final joseObject = JoseObject.fromCompactSerialization(token); | |
final header = joseObject.commonHeader; // commonProtectedHeader | |
// Key must exists in downloaded keys list | |
final key = keys[header.keyId]; | |
if (key == null) return false; | |
// Create a JSON Web Key for verifying the signature | |
final keyStore = new JsonWebKeyStore() | |
..addKey(JsonWebKey.fromJson(key)); | |
// Verify the App Check token's signature | |
final verified = await joseObject.verify(keyStore); | |
if (verified == false) return false; | |
// Ensure that the token's header has type JWT | |
if (header.type != "JWT") return false; | |
// Ensure that the token's header uses the algorithm RS256 | |
// if (header.algorithm != "RS256") return false; | |
// Extract the payload, check the algorithm | |
final content = await joseObject.getPayload(keyStore, allowedAlgorithms: ["RS256"]); | |
final payload = content.jsonContent; | |
// Ensure that the token is issued by Firebase App Check under your project | |
const issuer = 'https://firebaseappcheck.googleapis.com/$firebaseProjectNumber'; | |
if (payload["iss"] != issuer) return false; | |
// Ensure that the token has not expired | |
final exp = DateTime.fromMillisecondsSinceEpoch(payload["exp"] * 1000); // Expiration time | |
if (exp.isBefore(DateTime.now())) return false; // Must be in the future | |
final iat = DateTime.fromMillisecondsSinceEpoch(payload["iat"] * 1000); // Issued-at time | |
if (iat.isAfter(DateTime.now())) return false; // Must be in the past | |
// Ensure that the token's audience matches your project | |
const audience = [ | |
"projects/$firebaseProjectNumber", | |
"projects/$firebaseProjectId" | |
]; | |
if (!ListEquality().equals(payload["aud"], audience)) return false; | |
// Ensure that the token's subject matches your app's App ID | |
if (!firebaseAppIds.contains(payload["sub"])) return false; | |
return true; | |
} | |
// Download App Check JWK Set | |
Future<Map<String, Map<String, dynamic>>> fetchAppCheckKeys() async { | |
try { | |
const url = 'https://firebaseappcheck.googleapis.com/v1beta/jwks'; | |
final response = await Dio().get<Map<String, dynamic>>(url, | |
options: Options(responseType: ResponseType.json), | |
); | |
final data = response.data?["keys"]; | |
if (data == null) return {}; | |
// Convert list of keys into Map object | |
return Map.fromIterable(data, key: (key) => key["kid"]); | |
} catch (_) { | |
return {}; | |
} | |
} | |
// Usage | |
void main() async { | |
final bool checked = await appCheck("FIREBASE_APP_CHECK_TOKEN"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment