Verify Firebase auth JWT with PHP Laravel
A few months ago, I wrote about how to verify Firebase auth JWT using Ruby. Similarly, Firebase doesn't provide an official SDK for PHP. In this post, I will walk you through how to decode and verify Firebase JWT using PHP. It will use a few Laravel classes for caching, http client, and string manipulations, but the approach I will show here is framework-agnostic.
Overall flow
- The client app retrieves the Firebase ID Token (JWT) and send it to the backend server through
Authorization
HTTP header - The backend server decodes and verifies the JWT following this guide
I'm not going to cover how to retrieve the token on the client side since it's very straightforward. This post is focused on decoding and verifying JWT on the backend.
TL;DR
I created the FirebaseToken
class with verify()
method that verifies JWT and returns the docoded payload which contains the authenticated user data. You can find the full codebase here. Let me explain how it works step by step.
Install firebase/php-jwt package
First, let's install firebase/php-jwt which we will use for decoding and verifying JWT.
$ composer require firebase/php-jwt
FirebaseToken class
Next, create the FirebaseToken
class. The constructor takes the $token
which is the actual JWT.
/**
* Firebase ID token.
*
* @var string
*/
private string $token;
/**
* @param string $token
*/
public function __construct(string $token)
{
$this->token = $token;
}
Fetch public keys for verifying JWT
In the verify()
method, we will use the JWT::decode()
that firebase/php-jwt
provides. It takes public keys as the second argument which will be used for verifying token was signed by the right private key.
/**
* 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;
}
Grab the public key from here and pass it to JWT::decode()
method. As you can see, it caches the response (i.e. public keys) to avoid downloading it every time. For the cache TTL, use the max-age
in the Cache-Control
header of the response.
/**
* Fetch JWT public keys.
*
* @return array
* @throws Exception
*/
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;
}
Decode and verify JWT
Now, let's decode and verify the JWT calling the JWT::decode()
method.
$payload = JWT::decode($this->token, $keys, self::ALLOWED_ALGOS);
The third argument is an array of signing algorithms used in the JWT. Firebase uses RS256
for signing JWT.
/**
* The list of allowed signing algorithms used in the JWT.
*
* @var array
*/
const ALLOWED_ALGOS = ['RS256'];
We need to verify the header and payload following Firebase's official guide. JWT::decode()
verifies most of the attributes (see here for details), but we need to verify aud
, iss
, and sub
manually like below.
/**
* Validate decoded payload.
*
* @param object $payload
* @param string $projectId Firebase project id
* @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.');
}
}
Let's use it!
Done! Finally, we can use the FirebaseToken
class to decode and verify Firebase JWT.
// Retrieve Authorization header
$token = $request->bearerToken();
$payload = (new FirebaseToken($token))->verify(
config('services.firebase.project_id')
);
$payload->user_id;
I added the following config in config/services.php
.
'firebase' => [
'project_id' => env('FIREBASE_PROJECT_ID'),
]
You can use it for your custom auth guard in your Laravel application. The full codebase can be found here.
If you've got any questions or comments, please comment in this Twitter thread!