Skip to content

Instantly share code, notes, and snippets.

@starkdmi
Last active March 12, 2023 21:25
Show Gist options
  • Save starkdmi/d8e3a7c76deebe41300dfc7e51ea7c46 to your computer and use it in GitHub Desktop.
Save starkdmi/d8e3a7c76deebe41300dfc7e51ea7c46 to your computer and use it in GitHub Desktop.
Verify JWT token of authenticated Firebase user
import 'package:dio/dio.dart'; // default `http` package can be used as well
import 'package:jose/jose.dart'; // BSD-3-Clause, use `dart_jsonwebtoken` package in case of MIT license
const firebaseProjectId = "FIREBASE_PROJECT_ID";
Map<String, String> googleSecureTokens = {}; // { "KeyId": "PublicKey" } from public Google website
DateTime? googleTokensExpirationDate;
// Verify Firebase JWT token
// https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library
Future<bool> validateToken(String token) async {
final joseObject = JoseObject.fromCompactSerialization(token);
// Refresh tokens
await updateSecureTokensIfNeeded();
// Verify the header
final header = joseObject.commonHeader; // commonProtectedHeader
// Key Id must exists in googleSecureTokens
final keyId = header.keyId;
if (googleSecureTokens[keyId] == null) return false;
// Get Google PEM certificate from local cache
final certificate = googleSecureTokens[keyId]!;
final publicKey = JsonWebKey.fromPem(certificate, keyId: keyId);
// Init Key Store with the public key
final keyStore = new JsonWebKeyStore()
..addKey(publicKey);
// Signature verification
final verified = await joseObject.verify(keyStore);
if (!verified) return false;
// Extract and verify the payload
final content = await joseObject.getPayload(keyStore, allowedAlgorithms: ["RS256"]);
final payload = content.jsonContent;
// Expiration time - Must be in the future
final exp = DateTime.fromMillisecondsSinceEpoch(payload["exp"] * 1000);
if (exp.isBefore(DateTime.now())) return false;
// Issued-at time - Must be in the past
final iat = DateTime.fromMillisecondsSinceEpoch(payload["iat"] * 1000);
if (iat.isAfter(DateTime.now())) return false;
// Authentication time - Must be in the past
final authTime = DateTime.fromMillisecondsSinceEpoch(payload["auth_time"] * 1000);
if (authTime.isAfter(DateTime.now())) return false;
// Audience - Must be the Firebase project ID
if (payload["aud"] != firebaseProjectId) return false;
// Issuer - Must be "https://securetoken.google.com/<projectId>"
if (payload["iss"] != 'https://securetoken.google.com/$firebaseProjectId') return false;
// Subject (uid of user or device) - Must be a non-empty string
final uid = payload["sub"];
if (uid == "") return false;
// Unique ID of authenticated user
// print(uid);
return true;
}
// Update Google public keys
Future<bool> updateSecureTokensIfNeeded() async {
if (googleTokensExpirationDate == null || googleTokensExpirationDate!.isBefore(DateTime.now())) {
try {
const url = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
final response = await Dio().get(url, options: Options(responseType: ResponseType.json));
// Update local keys
googleSecureTokens = Map.castFrom(response.data);
// Get max-age from the Cache-Control
final cacheControl = response.headers["cache-control"]?[0];
if (cacheControl == null) return true;
final pattern = RegExp(r"max-age=(?<exp>\d+)");
final maxAge = pattern.firstMatch(cacheControl)?.namedGroup('exp');
if (maxAge == null) return true;
final seconds = int.tryParse(maxAge);
if (seconds == null) return true;
// Update local cache expiration date
googleTokensExpirationDate = DateTime.now().add(Duration(seconds: seconds));
} catch (error) {
return false;
}
}
return true;
}
// Usage
void main() async {
final bool authenticated = await validateToken("FIREBASE_JWT_TOKEN");
}
@neparij
Copy link

neparij commented Jan 19, 2023

Oh my God!
Thank you, pal!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment