Last active
September 5, 2024 03:48
-
-
Save starkdmi/d8e3a7c76deebe41300dfc7e51ea7c46 to your computer and use it in GitHub Desktop.
Verify JWT token of authenticated Firebase user
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: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/[email protected]'; | |
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"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Oh my God!
Thank you, pal!