31

I have a shared hosting plan which has only PHP(no Java, no node.js). I need to send firebase ID token from my android app and verify it by PHP-JWT.

I am following the tutorial: Verify Firebase ID tokens

It says:

If your backend is in a language that doesn't have an official Firebase Admin SDK, you can still verify ID tokens. First, find a third-party JWT library for your language. Then, verify the header, payload, and signature of the ID token.

I found that library: Firebase-PHP-JWT. In gitHub example; i couldn't understand the

$key part:

`$key = "example_key";` 

and

$token part:

`$token = array(
    "iss" => "http://example.org",
    "aud" => "http://example.com",
    "iat" => 1356999524,
    "nbf" => 1357000000
);`

My questions:

  1. What should be the $key variable?
  2. Why the &token variable is an array? Token which will be sent from mobile app is a String.
  3. If somebody could post a full example of verifying firebase ID with PHP-JWT, i would appreciate it.

EDIT:

Okey i got the point. GitHub example shows how to generate JWT code(encode) and how to decode it. In my case i need only decode the jwt which encoded by firebase. So, i need to use only this code:

$decoded = JWT::decode($jwt, $key, array('HS256'));

In this code part $jwt is the firebase ID token. For $key variable documentation says:

Finally, ensure that the ID token was signed by the private key corresponding to the token's kid claim. Grab the public key from https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com and use a JWT library to verify the signature. Use the value of max-age in the Cache-Control header of the response from that endpoint to know when to refresh the public keys.

I didn't understand how to pass this public keys to decode function. Keys are something like this:

"-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIIZ36AHgMyvnQwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTcw\nMjA4MDA0NTI2WhcNMTcwMjExMDExNTI2WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBANBNTpiQplOYizNeLbs+r941T392wiuMWr1gSJEVykFyj7fe\nCCIhS/zrmG9jxVMK905KwceO/FNB4SK+l8GYLb559xZeJ6MFJ7QmRfL7Fjkq7GHS\n0/sOFpjX7vfKjxH5oT65Fb1+Hb4RzdoAjx0zRHkDIHIMiRzV0nYleplqLJXOAc6E\n5HQros8iLdf+ASdqaN0hS0nU5aa/cPu/EHQwfbEgYraZLyn5NtH8SPKIwZIeM7Fr\nnh+SS7JSadsqifrUBRtb//fueZ/FYlWqHEppsuIkbtaQmTjRycg35qpVSEACHkKc\nW05rRsSvz7q1Hucw6Kx/dNBBbkyHrR4Mc/wg31kCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBAEuYEtvmZ4uReMQhE3P0iI4wkB36kWBe1mZZAwLA5A+U\niEODMVKaaCGqZXrJTRhvEa20KRFrfuGQO7U3FgOMyWmX3drl40cNZNb3Ry8rsuVi\nR1dxy6HpC39zba/DsgL07enZPMDksLRNv0dVZ/X/wMrTLrwwrglpCBYUlxGT9RrU\nf8nAwLr1E4EpXxOVDXAX8bNBl3TCb2fu6DT62ZSmlJV40K+wTRUlCqIewzJ0wMt6\nO8+6kVdgZH4iKLi8gVjdcFfNsEpbOBoZqjipJ63l4A3mfxOkma0d2XgKR12KAfYX\ncAVPgihAPoNoUPJK0Nj+CmvNlUBXCrl9TtqGjK7AKi8=\n-----END CERTIFICATE-----\n"

Do i need to convert this public key to something before pass it? I tried to remove all "\n" and "-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----"...But no luck. Still i get invalid signature error. Any advice?

Taha Sami
  • 1,565
  • 1
  • 16
  • 43
Eren
  • 2,583
  • 2
  • 31
  • 36
  • Firebase version info? Note that you don't verify tokens in PHP. You mint them there, send them to the client, and the client does the verification. – Kato Feb 08 '17 at 00:13
  • @Kato i use the last version. 'com.google.firebase:firebase-auth:10.0.1'. I didn't get your point. After client log in on mobile, firebase auth returns a token. I want to verify this token on the server-side with PHP to be sure that token generated by firebase or not. If the verification is ok, i will authorize client. – Eren Feb 08 '17 at 03:46
  • @eren130, do you know how often are the public verification keys changed? Should we cache them an hour, a day, a week? Thanks. – andreszs Jul 05 '18 at 01:13
  • 1
    @andreszs "Use the value of max-age in the Cache-Control header of the response from that endpoint to know when to refresh the public keys." – Eren Jul 05 '18 at 20:05

4 Answers4

30

HS256 is used only if you use a password to sign the token. Firebase uses RS256 when it issues a token, thus, you need the public keys from the given URL, and you need to set the algorithm to RS256.

Also note that the token you get in your application should not be an array but a string that has 3 parts: header, body, and signature. Each part is separated by a ., thus it gives you a simple string: header.body.signature

What you need to do in order to verify the tokens is downloading the public keys from the given URL regularly (check the Cache-Control header for that info) and saving it (the JSON) in a file, so you won't have to retrieve it every time you need to check the JWT. Then you can read in the file and decode the JSON. The decoded object can be passed to the JWT::decode(...) function. Here's a short sample:

$pkeys_raw = file_get_contents("cached_public_keys.json");
$pkeys = json_decode($pkeys_raw, true);

$decoded = JWT::decode($token, $pkeys, ["RS256"]);

Now the $decoded variable contains the payload of the token. Once you have the decoded object, you still need to verify it. According to the guide on ID token verification, you have to check the following things:

  • exp is in the future
  • iat is in the past
  • iss: https://securetoken.google.com/<firebaseProjectID>
  • aud: <firebaseProjectID>
  • sub is non-empty

So, for example, you can check iss like this (where FIREBASE_APP_ID is the app ID from the firebase console):

$iss_is_valid = isset($decoded->iss) && $decoded->iss === "https://securetoken.google.com/" . FIREBASE_APP_ID;

Here is a complete sample for refreshing the keys and retrieving them.

Disclaimer: I haven't tested it and this is basically for informational purposes only.

$keys_file = "securetoken.json"; // the file for the downloaded public keys
$cache_file = "pkeys.cache"; // this file contains the next time the system has to revalidate the keys

/**
 * Checks whether new keys should be downloaded, and retrieves them, if needed.
 */
function checkKeys()
{
    if (file_exists($cache_file)) {
        $fp = fopen($cache_file, "r+");

        if (flock($fp, LOCK_SH)) {
            $contents = fread($fp, filesize($cache_file));
            if ($contents > time()) {
                flock($fp, LOCK_UN);
            } elseif (flock($fp, LOCK_EX)) { // upgrading the lock to exclusive (write)
                // here we need to revalidate since another process could've got to the LOCK_EX part before this
                if (fread($fp, filesize($this->cache_file)) <= time()) {
                    $this->refreshKeys($fp);
                }
                flock($fp, LOCK_UN);
            } else {
                throw new \RuntimeException('Cannot refresh keys: file lock upgrade error.');
            }
        } else {
            // you need to handle this by signaling error
            throw new \RuntimeException('Cannot refresh keys: file lock error.');
        }

        fclose($fp);
    } else {
        refreshKeys();
    }
}

/**
 * Downloads the public keys and writes them in a file. This also sets the new cache revalidation time.
 * @param null $fp the file pointer of the cache time file
 */
function refreshKeys($fp = null)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HEADER, 1);

    $data = curl_exec($ch);

    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $headers = trim(substr($data, 0, $header_size));
    $raw_keys = trim(substr($data, $header_size));

    if (preg_match('/age:[ ]+?(\d+)/i', $headers, $age_matches) === 1) {
        $age = $age_matches[1];

        if (preg_match('/cache-control:.+?max-age=(\d+)/i', $headers, $max_age_matches) === 1) {
            $valid_for = $max_age_matches[1] - $age;
            ftruncate($fp, 0);
            fwrite($fp, "" . (time() + $valid_for));
            fflush($fp);
            // $fp will be closed outside, we don't have to

            $fp_keys = fopen($keys_file, "w");
            if (flock($fp_keys, LOCK_EX)) {
                fwrite($fp_keys, $raw_keys);
                fflush($fp_keys);
                flock($fp_keys, LOCK_UN);
            }
            fclose($fp_keys);
        }
    }
}

/**
 * Retrieves the downloaded keys.
 * This should be called anytime you need the keys (i.e. for decoding / verification).
 * @return null|string
 */
function getKeys()
{
    $fp = fopen($keys_file, "r");
    $keys = null;

    if (flock($fp, LOCK_SH)) {
        $keys = fread($fp, filesize($keys_file));
        flock($fp, LOCK_UN);
    }

    fclose($fp);

    return $keys;
}

The best thing would be scheduling a cronjob to call checkKeys() whenever needed, but I don't know if your provider allows that. Instead of that, you can do this for every request:

checkKeys();
$pkeys_raw = getKeys(); // check if $raw_keys is not null before using it!
Gergely Kőrössy
  • 5,620
  • 3
  • 28
  • 44
  • thanks! Saved my life...one last thing. Could you edit your answer and add code sample that cache public keys according to "Cache-Control: max-age" and renew them when max-age time expires? It would be complete sample source for verifying firebase ID. Sorry for my poor PHP knowladge...By the way, already two days i have been playing around with firebase auth. "Cache-Control" header from giveng url was always "max-age=0". So, why need to cache? – Eren Feb 09 '17 at 09:36
  • I just opened it up in both Firefox and Chrome and they show a valid max-age value. How do you retrieve the headers? – Gergely Kőrössy Feb 09 '17 at 11:56
  • I use "Live HTTP Headers" extension on Firefox. Sorry, just now i saw, there are two seperate headers: http://prntscr.com/e6ht8d – Eren Feb 09 '17 at 12:13
  • @eren130 I meant how do you retrieve the URL in PHP? Do you use cURL? – Gergely Kőrössy Feb 09 '17 at 12:14
  • i simply use $pkeys_raw = file_get_contents($url); – Eren Feb 09 '17 at 12:18
  • Is cURL available on your shared host? Check it like this: http://stackoverflow.com/a/9183272 – Gergely Kőrössy Feb 09 '17 at 12:25
  • @eren130 Added a sample retrieval, check if it works, I haven't had time to check it. – Gergely Kőrössy Feb 10 '17 at 02:13
  • @gergely-kőrössy Your answer was most helpful but I have a slightly silly question: Once I have the decoded payload and have checked that the individual components are valid (e.g. have not expired etc.) do I need to actually contact my Firebase database and check that this user is valid? Or is this process inherently secure and I can just right away use the UID in my MySQL database? What's to stop somebody just creating a jwt with another firebase UID since all of the info here is essentially available to the public? Sorry if I'm misunderstanding, thank you! – Josh Mar 04 '18 at 20:06
  • 1
    @Josh I don't know if you know about asymmetric encryption or signing / verifying things using keys but in a nutshell, one would need the _private_ key(s) to create tokens like this that can be verified using the _public_ key(s) that you download and use in this sample. The private keys are not publicly known, they cannot be accessed in any way, and are secret, hence _private_. So, to answer your question: **yes, if you verified that this token was coming from Firebase, you could safely use the UID in your database** without further checks towards the Firebase database. – Gergely Kőrössy Mar 04 '18 at 23:18
  • @GergelyKőrössy good that you mentioned it, didn't know that the `project-id` was a `secret-key`, I need to change that really fast before someone detects that it's equal to my App-name ;-) – Top-Master Jan 16 '20 at 12:05
  • Hey @GergelyKőrössy, I'm using your code and it's returning this error: `$keyOrKeyArray must be an instance of Firebase\\JWT\\Key key or an array of Firebase\\JWT\\Key keys in...` so I tried using the new method `JWT::decode($token, new Key($pkeys, "RS256"));` but now I get this error: `$keyMaterial must be a string, resource, or OpenSSLAsymmetricKey in ...firebase\\php-jwt\\src\\Key.php on line 27`. Can you help me with that? Thanks in advance! – madsongr Jun 24 '22 at 18:55
7

Working example of accepted answer. differences of note:

  • Tested and Working

  • works in non-class environments

  • More code showing how to use it for Firebase (simple, one-liner to send the code for verification)

  • UnexpectedValueException covers all sorts of errors you might see (such as expired/invalid keys)

  • Well commented and easy-to-follow

  • returns an array of VERIFIED data from the Firebase Token (you can securely use this data for anything you need)

This is basically a broken-out, easy-to-read/understand PHP version of https://firebase.google.com/docs/auth/admin/verify-id-tokens

NOTE: You can use the getKeys(), refreshKeys(), checkKeys() functions to generate keys for use in any secure api situation (mimicking the features of 'verify_firebase_token' function with your own).

USE:

$verified_array = verify_firebase_token(<THE TOKEN FROM FIREBASE>)

THE CODE:

$keys_file = "securetoken.json"; // the file for the downloaded public keys
$cache_file = "pkeys.cache"; // this file contains the next time the system has to revalidate the keys
//////////  MUST REPLACE <YOUR FIREBASE PROJECTID> with your own!
$fbProjectId = <YOUR FIREBASE PROJECTID>;

/////// FROM THIS POINT, YOU CAN COPY/PASTE - NO CHANGES REQUIRED
///  (though read through for various comments!)
function verify_firebase_token($token = '')
{
    global $fbProjectId;
    $return = array();
    $userId = $deviceId = "";
    checkKeys();
    $pkeys_raw = getKeys();
    if (!empty($pkeys_raw)) {
        $pkeys = json_decode($pkeys_raw, true);
        try {
            $decoded = \Firebase\JWT\JWT::decode($token, $pkeys, ["RS256"]);
            if (!empty($_GET['debug'])) {
                echo "<hr>BOTTOM LINE - the decoded data<br>";
                print_r($decoded);
                echo "<hr>";
            }
            if (!empty($decoded)) {
                // do all the verifications Firebase says to do as per https://firebase.google.com/docs/auth/admin/verify-id-tokens
                // exp must be in the future
                $exp = $decoded->exp > time();
                // ist must be in the past
                $iat = $decoded->iat < time();
                // aud must be your Firebase project ID
                $aud = $decoded->aud == $fbProjectId;
                // iss must be "https://securetoken.google.com/<projectId>"
                $iss = $decoded->iss == "https://securetoken.google.com/$fbProjectId";
                // sub must be non-empty and is the UID of the user or device
                $sub = $decoded->sub;
                if ($exp && $iat && $aud && $iss && !empty($sub)) {
                    // we have a confirmed Firebase user!
                    // build an array with data we need for further processing
                    $return['UID'] = $sub;
                    $return['email'] = $decoded->email;
                    $return['email_verified'] = $decoded->email_verified;
                    $return['name'] = $decoded->name;
                    $return['picture'] = $decoded->photo;
                } else {
                    if (!empty($_GET['debug'])) {
                        echo "NOT ALL THE THINGS WERE TRUE!<br>";
                        echo "exp is $exp<br>ist is $iat<br>aud is $aud<br>iss is $iss<br>sub is $sub<br>";
                    }
                    /////// DO FURTHER PROCESSING IF YOU NEED TO
                    // (if $sub is false you may want to still return the data or even enter the verified user into the database at this point.)
                }
            }
        } catch (\UnexpectedValueException $unexpectedValueException) {
            $return['error'] = $unexpectedValueException->getMessage();
            if (!empty($_GET['debug'])) {
                echo "<hr>ERROR! " . $unexpectedValueException->getMessage() . "<hr>";
            }
        }
    }
    return $return;
}
/**
* Checks whether new keys should be downloaded, and retrieves them, if needed.
*/
function checkKeys()
{
    global $cache_file;
    if (file_exists($cache_file)) {
        $fp = fopen($cache_file, "r+");
        if (flock($fp, LOCK_SH)) {
            $contents = fread($fp, filesize($cache_file));
            if ($contents > time()) {
                flock($fp, LOCK_UN);
            } elseif (flock($fp, LOCK_EX)) { // upgrading the lock to exclusive (write)
                // here we need to revalidate since another process could've got to the LOCK_EX part before this
                if (fread($fp, filesize($cache_file)) <= time()) 
                {
                    refreshKeys($fp);
                }
                flock($fp, LOCK_UN);
            } else {
                throw new \RuntimeException('Cannot refresh keys: file lock upgrade error.');
            }
        } else {
            // you need to handle this by signaling error
        throw new \RuntimeException('Cannot refresh keys: file lock error.');
        }
        fclose($fp);
    } else {
        refreshKeys();
    }
}

/**
 * Downloads the public keys and writes them in a file. This also sets the new cache revalidation time.
 * @param null $fp the file pointer of the cache time file
 */
function refreshKeys($fp = null)
{
    global $keys_file;
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HEADER, 1);
    $data = curl_exec($ch);
    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $headers = trim(substr($data, 0, $header_size));
    $raw_keys = trim(substr($data, $header_size));
    if (preg_match('/age:[ ]+?(\d+)/i', $headers, $age_matches) === 1) 
    {
        $age = $age_matches[1];
        if (preg_match('/cache-control:.+?max-age=(\d+)/i', $headers, $max_age_matches) === 1) {
            $valid_for = $max_age_matches[1] - $age;
            $fp = fopen($keys_file, "w");
            ftruncate($fp, 0);
            fwrite($fp, "" . (time() + $valid_for));
            fflush($fp);
            // $fp will be closed outside, we don't have to
            $fp_keys = fopen($keys_file, "w");
            if (flock($fp_keys, LOCK_EX)) {
                fwrite($fp_keys, $raw_keys);
                fflush($fp_keys);
                flock($fp_keys, LOCK_UN);
            }
            fclose($fp_keys);
        }
    }
}

/**
 * Retrieves the downloaded keys.
 * This should be called anytime you need the keys (i.e. for decoding / verification).
 * @return null|string
 */
function getKeys()
{
   global $keys_file;
    $fp = fopen($keys_file, "r");
    $keys = null;
    if (flock($fp, LOCK_SH)) {
        $keys = fread($fp, filesize($keys_file));
        flock($fp, LOCK_UN);
    }
    fclose($fp);
    return $keys;
}
Apps-n-Add-Ons
  • 2,026
  • 1
  • 17
  • 28
  • Hi, could you explain what `$subToken` inside of your `function get_userId_by_deviceToken($subToken)` is used for here? Is that the idtoken? –  Apr 03 '18 at 13:14
  • 1
    The $subToken is referring to the $decoded->sub (see the line $userId = get_userId_by_deviceToken($decoded->sub);) If you are storing the subscriber token from Firebase, you can use that function to pull the userId from your database. You don't have to use $decoded->sub, you could call your db to get the userId from the $decoded->email or whatever. – Apps-n-Add-Ons Apr 03 '18 at 13:24
  • Ah, I see. I missed that part; I thought that the userId was being returned in an array with the rest of the information, and I was going to use that to add the information to my database. Do I still need the `get_userId_by_deviceToken` function? I was planning on sending my idtoken using a HTTP POST request like the one used here: `https://developers.google.com/identity/sign-in/web/backend-auth`. –  Apr 03 '18 at 13:31
  • In the end here, I am returning the userId in the array - but, first, you have to get it from your internal server! (Firebase doesn't know it.... :) You could certainly not use it (as I said in previous comment). This set of functions provides a more 'open' way to see what is going on - you could also (probably - not tested by me with Firebase) use the Google backend-auth (https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token) instead of this code. I like the method here as we can see what is going on. – Apps-n-Add-Ons Apr 03 '18 at 13:45
  • The internal server being my database? (Sorry for all the questions -- I'm quite new and just want to make sure I understand what's going on before utilizing code). How can I get it from my internal server before putting it into my database from the array? I hope my question makes sense. –  Apr 03 '18 at 13:51
  • 1
    I changed the code - sorry that part confused you....... (and, to note, the code does say "/////// DO FURTHER PROCESSING IF YOU NEED TO // (if $sub is false you may want to still return the data or even enter the verified user into the database at this point.) }", so, you wouldn't "get it from my internet server before..." – Apps-n-Add-Ons Apr 03 '18 at 14:04
  • Thanks, your example works (almost) perfectly! You should not assume that the decoded token will always contain `email`, `name`, `picture`, etc. because in the case of registration via Phone, none of those fields are present, so your sample code triggers an error. Also, as new auth sources are added, you don't know what fields you may get in that payload, better return the entire `$decoded` array to prevent errors. – andreszs Jul 05 '18 at 01:18
  • 2
    Also, your `refreshKeys` method is writing the keys revalidation time in `securetoken.json` instead of `pkeys.cache`! therefore you are never using the cache. The sample as is, keeps redownloading the keys again every single time. To fix it use `$fp = fopen($cache_file, "w");` before `ftruncate($fp, 0);` and declare `global $cache_file;` on that function. Last: `ftruncate` is unneeded when you `fopen` with mode `w`, which auto-truncates the file. – andreszs Jul 05 '18 at 23:11
  • Thanks a lot. However `age:` was not present in the headers, so I replaced the first preg_match in refreshKeys with this regex : `/max-age=(\d+)/` – JBA Jun 04 '19 at 22:10
  • Thanks! This works perfect for servers running php 5.6 – Andres SK Apr 28 '20 at 23:04
  • Please look at this https://stackoverflow.com/questions/68658303/firebase-auth-with-supabase-jwt-token-flutter – Noobdeveloper Aug 05 '21 at 07:02
7

Instead of doing it all manually, you can take a look at this library:
Firebase Tokens or even Firebase Admin SDK for PHP. Caching stuff etc. is already implemented, just take a look at the docs.

Basically you would simply do the following using Firebase Tokens Library:

use Firebase\Auth\Token\HttpKeyStore;
use Firebase\Auth\Token\Verifier;
use Symfony\Component\Cache\Simple\FilesystemCache;

$cache = new FilesystemCache();
$keyStore = new HttpKeyStore(null, $cache);
$verifier = new Verifier($projectId, $keyStore);

    try {
        $verifiedIdToken = $verifier->verifyIdToken($idToken);

        // "If all the above verifications are successful, you can use the subject 
        // (sub) of the ID token as the uid of the corresponding user or device. (see https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library)
        echo $verifiedIdToken->getClaim('sub'); // "a-uid"
    } catch (\Firebase\Auth\Token\Exception\ExpiredToken $e) {
        echo $e->getMessage();
    } catch (\Firebase\Auth\Token\Exception\IssuedInTheFuture $e) {
        echo $e->getMessage();
    } catch (\Firebase\Auth\Token\Exception\InvalidToken $e) {
        echo $e->getMessage();
    }
baris1892
  • 981
  • 1
  • 16
  • 29
2

If anyone is still interested, @CFP Support's answer is quite good for servers using PHP 5.6, but it does have some bugs while trying to cache the expiration time of the current saved public keys. I've taken that code, and made the necessary corrections:

Requirements in composer.json

{
    "require" : {
        "firebase/php-jwt": "5.2.0"
    }
}

Usage

<?
$verified = verify_firebase_token(<THE TOKEN FROM FIREBASE>);
?>

Functions

<?
# the file for the downloaded public keys
$jwt['keys'] = 'jwt.publickeys.json';

# this file contains the next time the system has to revalidate the keys
$jwt['cache'] = 'jwt.publickeys.cache';

# project ID
$jwt['project_id'] = YOUR_FIREBASE_PROJECT_ID;

# verify token
function verify_firebase_token($token) {
    global $jwt;
    $return = array();
    jwt_check_keys();
    $keys_raw = jwt_get_keys();
    if(!empty($keys_raw)) {
        $keys = json_decode($keys_raw, true);
        try {
            $decoded = \Firebase\JWT\JWT::decode($token, $keys, ['RS256']);
            if(!empty($decoded)) {
                # follow best practices verification-wise
                # https://firebase.google.com/docs/auth/admin/verify-id-tokens

                # exp must be in the future
                $exp = $decoded->exp > time();
                # ist must be in the past
                $iat = $decoded->iat < time();
                # aud must be firebase project ID
                $aud = $decoded->aud == $jwt['project_id'];
                # iss must be https://securetoken.google.com/<projectID>
                $iss = $decoded->iss == 'https://securetoken.google.com/'.$jwt['project_id'];
                # sub must be non-empty and is the UID of the user or device
                $sub = $decoded->sub;
                # check all items
                if($exp && $iat && $aud && $iss && !empty($sub)) {
                    # confirmed firebase user
                    $return['user']['uid'] = $sub;
                    // $return['user']['email'] = $decoded->email;
                    // $return['user']['name'] = $decoded->name;
                    // $return['user']['picture'] = $decoded->picture;
                    // $return['all'] = $decoded;
                } else {
                }
            }
        } catch (\UnexpectedValueException $unexpectedValueException) {
            $return['error'] = $unexpectedValueException->getMessage();
            //$unexpectedValueException->getMessage()
        }
    }
    return $return;
}

# checks whether new keys should be downloaded
# retrieves them if needed
function jwt_check_keys() {
    global $jwt;
    if(file_exists($jwt['cache'])) {
        $fp_cache = fopen($jwt['cache'], 'r+');
        if(flock($fp_cache, LOCK_SH)) {
            $cachetime = fread($fp_cache, filesize($jwt['cache']));
            if($cachetime > time()) {
                # still valid - do nothing
                flock($fp_cache, LOCK_UN);
            } elseif(flock($fp_cache, LOCK_EX)) {
                # expired - refresh public keys
                jwt_refresh_keys();
                flock($fp_cache, LOCK_UN);
            } else {
                throw new \RuntimeException('Cannot refresh keys: file lock upgrade error.');
            }
        } else {
            throw new \RuntimeException('Cannot refresh keys: file lock error.');
        }
        fclose($fp_cache);
    } else {
        # refresh public keys
        jwt_refresh_keys();
    }
}

# downloads the public keys and writes them in a file
# sets the new cache revalidation time
function jwt_refresh_keys() {
    global $jwt;
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HEADER, 1);
    $data = curl_exec($ch);
    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $headers = trim(substr($data, 0, $header_size));
    $raw_keys = trim(substr($data, $header_size));
    if(preg_match('/max-age=(\d+)/', $headers, $age_matches) === 1) {
        # update new cache expiration timestamp
        $fp_cache = fopen($jwt['cache'], 'w');
        $age = $age_matches[1];
        fwrite($fp_cache, ''.(time() + $age));
        fflush($fp_cache);

        # update public keys
        $fp_keys = fopen($jwt['keys'], 'w');
        if(flock($fp_keys, LOCK_EX)) {
            fwrite($fp_keys, $raw_keys);
            fflush($fp_keys);
            flock($fp_keys, LOCK_UN);
        }
        fclose($fp_keys);
    }
}

# retrieves the downloaded keys
# this should be called anytime you need the keys (i.e. for decoding / verification)
function jwt_get_keys() {
    global $jwt;
    $fp = fopen($jwt['keys'], 'r');
    $keys = null;
    if(flock($fp, LOCK_SH)) {
        $keys = fread($fp, filesize($jwt['keys']));
        flock($fp, LOCK_UN);
    }
    fclose($fp);
    return $keys;
}
?>
Andres SK
  • 10,779
  • 25
  • 90
  • 152
  • Please look at this https://stackoverflow.com/questions/68658303/firebase-auth-with-supabase-jwt-token-flutter – Noobdeveloper Aug 05 '21 at 07:03
  • 1
    @Andres SK thanks for your code, its very helpful. It appears that php-jwt has updated their and its not working anymore. You might could release an update please ? – user5441400 Aug 08 '22 at 20:08