Skip to content

Instantly share code, notes, and snippets.

@starkdmi
Last active March 13, 2022 15:44
Show Gist options
  • Save starkdmi/e9be666330e6141c03ec7734eaafcd8b to your computer and use it in GitHub Desktop.
Save starkdmi/e9be666330e6141c03ec7734eaafcd8b to your computer and use it in GitHub Desktop.
Verify Firebase App Check token
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