web-dev-qa-db-fra.com

Comment vérifier le jeton ID Firebase avec PHP (JWT)?

J'ai un plan d'hébergement partagé qui n'a que PHP (pas de Java, pas de node.js). Je dois envoyer un jeton d'identification Firebase depuis mon Android et le vérifier par PHP-JWT.

Je suis le tutoriel: Vérifier les jetons ID Firebase

Ça dit:

"Si votre serveur principal est dans une langue qui n'a pas de SDK administrateur Firebase officiel, vous pouvez toujours vérifier les jetons d'identification. Tout d'abord, recherchez une bibliothèque JWT tierce pour votre langue. Ensuite, vérifiez l'en-tête, la charge utile et la signature de le jeton d'identification. "

J'ai trouvé cette bibliothèque: Firebase-PHP-JWT . Dans l'exemple gitHub; je ne pouvais pas comprendre le

$ clé:

`$key = "example_key";` 

et

$ token part:

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

Mes questions:

  1. Quelle devrait être la variable $ key ?
  2. Pourquoi la variable & token est un tableau? Le jeton qui sera envoyé depuis l'application mobile est une chaîne.
  3. Si quelqu'un pouvait poster un exemple complet de vérification de l'ID Firebase avec PHP-JWT, je l'apprécierais.

MODIFIER:

Okey j'ai compris. L'exemple GitHub montre comment générer du code JWT (encoder) et comment le décoder. Dans mon cas, il me suffit de décoder le jwt qui a été encodé par firebase. Donc, je dois utiliser uniquement ce code:

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

Dans cette partie de code $ jwt est le jeton d'ID Firebase. Pour $ key la documentation des variables dit:

Enfin, assurez-vous que le jeton d'identification a été signé par la clé privée correspondant à la revendication d'enfant du jeton. Récupérez la clé publique à partir de https://www.googleapis.com/robot/v1/metadata/x509/[email protected] et utilisez une bibliothèque JWT pour vérifier la signature. Utilisez la valeur de max-age dans l'en-tête Cache-Control de la réponse de ce point de terminaison pour savoir quand actualiser les clés publiques.

Je n'ai pas compris comment passer ces clés publiques pour décoder la fonction. Les clés sont quelque chose comme ça:

" ----- 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 + Cmv_CKKNK

Dois-je convertir cette clé publique en quelque chose avant de la transmettre? J'ai essayé de supprimer tout "\ n" et "----- BEGIN CERTIFICATE ----- ", " ----- COMMENCER LE CERTIFICAT ----- " ... Mais pas de chance. Je reçois toujours une erreur de signature non valide. Aucun conseil?

19
eren130

HS256 est utilisé uniquement si vous utilisez un mot de passe pour signer le jeton. Firebase utilise RS256 lorsqu'il émet un jeton, vous avez donc besoin des clés publiques de l'URL donnée et vous devez définir l'algorithme sur RS256.

Notez également que le jeton que vous obtenez dans votre application ne doit pas être un tableau mais une chaîne composée de 3 parties: header, body et signature. Chaque partie est séparée par un ., Il vous donne donc une chaîne simple: header.body.signature

Ce que vous devez faire pour vérifier les jetons est de télécharger régulièrement les clés publiques à partir de RL donnée (vérifiez l'en-tête Cache-Control Pour ces informations) et de les enregistrer (le JSON) dans un fichier, vous n'aurez donc pas à le récupérer à chaque fois que vous aurez besoin de vérifier le JWT. Ensuite, vous pouvez lire le fichier et décoder le JSON. L'objet décodé peut être passé à la fonction JWT::decode(...). Voici un petit échantillon:

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

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

Maintenant, la variable $decoded Contient la charge utile du jeton. Une fois que vous avez l'objet décodé, vous devez toujours le vérifier. Selon le guide sur la vérification des jetons d'identification, vous devez vérifier les choses suivantes:

  • exp est dans le futur
  • iat appartient au passé
  • iss: https://securetoken.google.com/<firebaseProjectID>
  • aud: <firebaseProjectID>
  • sub n'est pas vide

Ainsi, par exemple, vous pouvez vérifier iss comme ceci (où FIREBASE_APP_ID Est l'ID d'application de la console Firebase):

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

Voici un exemple complet pour actualiser les clés et les récupérer.

Avis de non-responsabilité: je ne l'ai pas testé et c'est essentiellement à des fins d'information uniquement.

$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/[email protected]");
    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;
}

La meilleure chose serait de planifier un cronjob pour appeler checkKeys() chaque fois que nécessaire, mais je ne sais pas si votre fournisseur le permet. Au lieu de cela, vous pouvez le faire pour chaque demande:

checkKeys();
$pkeys_raw = getKeys(); // check if $raw_keys is not null before using it!
18
Gergely Kőrössy

Au lieu de tout faire manuellement, vous pouvez jeter un œil à cette bibliothèque:
Tokens Firebase ou même SDK Admin Firebase pour PHP . La mise en cache des trucs, etc. est déjà implémentée, jetez un œil aux documents.

Fondamentalement, vous feriez simplement ce qui suit en utilisant la bibliothèque de jetons Firebase:

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();
    }
5
baris1892

Exemple pratique de réponse acceptée. différences de note:

  • Testé et fonctionnel

  • fonctionne dans des environnements non-classe

  • Plus de code montrant comment l'utiliser pour Firebase (simple, une ligne pour envoyer le code pour vérification)

  • UnexpectedValueException couvre toutes sortes d'erreurs que vous pourriez voir (telles que les clés expirées/invalides)

  • Bien commenté et facile à suivre

  • renvoie un tableau de données VÉRIFIÉES à partir du jeton Firebase (vous pouvez utiliser ces données en toute sécurité pour tout ce dont vous avez besoin)

Il s'agit essentiellement d'une version détaillée et facile à lire/à comprendre PHP de https://firebase.google.com/docs/auth/admin/verify-id -tokens

REMARQUE: vous pouvez utiliser les fonctions getKeys (), refreshKeys (), checkKeys () pour générer des clés à utiliser dans n'importe quelle situation d'API sécurisée (imitant les fonctionnalités de la fonction 'verify_firebase_token' avec la vôtre).

UTILISATION:

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

LE 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/[email protected]");
    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;
}
5
CFP Support