Skip to content

Instantly share code, notes, and snippets.

@avosalmon
Created August 22, 2021 16:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save avosalmon/b4fd7bb5c4666ed39cdc06c9767afb84 to your computer and use it in GitHub Desktop.
Save avosalmon/b4fd7bb5c4666ed39cdc06c9767afb84 to your computer and use it in GitHub Desktop.
<?php
namespace App\Services;
use UnexpectedValueException;
use Firebase\JWT\JWT;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class FirebaseToken
{
/**
* The list of allowed signing algorithms used in the JWT.
*
* @var array
*/
const ALLOWED_ALGOS = ['RS256'];
/**
* The public key used for verifying that the token is signed by the right private key.
*
* @var string
*/
const PUBLIC_KEY_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
/**
* The cache key for Firebase JWT public keys.
*
* @var string
*/
const CACHE_KEY = 'FIREBASE_JWT_PUBLIC_KEYS';
/**
* Firebase ID token.
*
* @var string
*/
private string $token;
/**
* @param string $token
*/
public function __construct(string $token)
{
$this->token = $token;
}
/**
* Verify the ID token and return the decoded payload.
*
* @param string $projectId
* @return object
* @throws UnexpectedValueException|Exception
*/
public function verify(string $projectId): object
{
$keys = $this->getPublicKeys();
$payload = JWT::decode($this->token, $keys, self::ALLOWED_ALGOS);
$this->validatePayload($payload, $projectId);
return $payload;
}
/**
* Fetch JWT public keys.
*
* @return array
*/
private function getPublicKeys(): array
{
if (Cache::has(self::CACHE_KEY)) {
return Cache::get(self::CACHE_KEY);
}
$response = Http::get(self::PUBLIC_KEY_URL);
if (!$response->successful()) {
throw new \Exception('Failed to fetch JWT public keys.');
}
$publicKeys = $response->json();
$cacheControl = $response->header('Cache-Control');
$maxAge = Str::of($cacheControl)->match('/max-age=(\d+)/');
Cache::put(self::CACHE_KEY, $publicKeys, now()->addSeconds($maxAge));
return $publicKeys;
}
/**
* Validate decoded payload.
*
* @param object $payload
* @param string $projectId
* @return void
* @throws UnexpectedValueException
*
* @see https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library
*/
private function validatePayload(object $payload, string $projectId): void
{
if ($payload->aud !== $projectId) {
throw new UnexpectedValueException("Invalid audience: {$payload->aud}");
}
if ($payload->iss !== "https://securetoken.google.com/{$projectId}") {
throw new UnexpectedValueException("Invalid issuer: {$payload->iss}");
}
// `sub` corresponds to the `uid` of the Firebase user.
if (empty($payload->sub)) {
throw new UnexpectedValueException('Payload subject is empty.');
}
}
}
@g-calcagno
Copy link

has defined 'alg', but and 'kid' ?

@avosalmon
Copy link
Author

@g-calcagno The php-jwt library validates kid. So, we just need to pass public keys.
https://github.com/firebase/php-jwt/blob/master/src/JWT.php#L114-L123

@g-calcagno
Copy link

Good afternoon, thanks for the reply and the material. I have been trying to integrate it, but I had some problems probably due to my ignorance. The constant 'PUBLIC_KEY_URL' is obtained from the JSON (from the google service account) "client_x509_cert_url".
I overwritten my passport private key with the private key from the JSON and generated a public key from the private key. Even so, I keep getting a KID from the KEY [kid]. In the url provided by google, at first glance you can already see that no index of the key segments has the same key [id] as the private key, always returning a '' "kid" invalid, unable to lookup correct key " I do not quite understand how it works.

@avosalmon
Copy link
Author

@ g-calcagno Are you using Laravel Passport? I'm not using it but only using Firebase Auth.
Following this guide, I fetched the JSON that contains public keys and passed it to the JWT::decode method. It will decode the JWT and check whether the kid claim in the JWT header matches one of the keys in the JSON as mentioned in the guide.

@rochaeterno
Copy link

rochaeterno commented Jul 4, 2023

so, since Firebase-JWT has major break changes related to Key() i did some changes to this code to make this work well in my API, if you want i can send you this or open a PR if you allow me.
If anyone is looking dor this to use with today library there is the changes i did:
https://gist.github.com/rochaeterno/cfb1bb0c8987201c1e4b78d885f607f0

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