首页 文章

如何使用PHP(JWT)验证firebase ID令牌?

提问于
浏览
16

我有一个只有PHP(没有Java,没有node.js)的共享主机方案 . 我需要从我的Android应用程序发送firebase ID令牌并通过PHP-JWT验证它 .

我正在按照教程:Verify Firebase ID tokens

它说:

“如果您的后端使用的语言没有官方的Firebase Admin SDK,您仍然可以验证ID令牌 . 首先,找到适合您语言的第三方JWT库 . 然后,验证 Headers ,有效负载和签名ID令牌 . “

我发现那个库:Firebase-PHP-JWT . 在gitHub示例中;我无法理解

$关键部分:

`$key = "example_key";`

$ token部分:

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

我的问题:

  • $key 变量应该是什么?

  • 为什么 &token 变量是一个数组?将从移动应用程序发送的令牌是一个字符串 .

  • 如果有人可以发布用PHP-JWT验证firebase ID的完整示例,我将不胜感激 .

EDIT:

哦,我明白了 . GitHub示例演示了如何生成JWT代码(编码)以及如何解码它 . 在我的情况下,我只需要解码由firebase编码的jwt . 所以,我只需要使用这个代码:

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

在此代码中, $jwt 是firebase ID令牌 . 对于 $key 变量文档说:

最后,确保ID令牌由与令牌的孩子声明相对应的私钥签名 . 从https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com获取公钥,并使用JWT库验证签名 . 使用来自该 endpoints 的响应的Cache-Control标头中的max-age值来了解何时刷新公钥 .

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 \ nf8nAwLr1E4EpXxOVDXAX8bNBl3TCb2fu6DT62ZSmlJV4 0K wTRUlCqIewzJ0wMt6 \ nO8 6kVdgZH4iKLi8gVjdcFfNsEpbOBoZqjipJ63l4A3mfxOkma0d2XgKR12KAfYX \ ncAVPgihAPoNoUPJK0Nj CmvNlUBXCrl9TtqGjK7AKi8 = \ n ----- END CERTIFICATE ----- \ n“

我是否需要在传递之前将此公钥转换为某些内容?我试图删除所有 "\n""-----BEGIN CERTIFICATE-----""-----BEGIN CERTIFICATE-----" ......但没有运气 . 我仍然得到无效的签名错误 . 有什么建议?

3 回答

  • 17

    仅当您使用密码对令牌进行签名时才使用HS256 . Firebase在发出令牌时使用RS256,因此,您需要来自给定URL的公钥,并且您需要将算法设置为RS256 .

    另请注意,您在应用程序中获得的令牌不应该是一个数组,而是一个包含3个部分的字符串: headerbodysignature . 每个部分由 . 分隔,因此它为您提供了一个简单的字符串: header.body.signature

    为了验证令牌,您需要做的是定期从given URL下载公钥(检查该信息的 Cache-Control Headers )并将其保存(JSON)在文件中,这样您就不必每次都检索它你需要检查JWT的时间 . 然后,您可以读入文件并解码JSON . 解码后的对象可以传递给 JWT::decode(...) 函数 . 这是一个简短的样本:

    $pkeys_raw = file_get_contents("cached_public_keys.json");
    $pkeys = json_decode($pkeys_raw, true);
    
    $decoded = JWT::decode($token, $pkeys, ["RS256"]);
    

    现在 $decoded 变量包含令牌的有效负载 . 获得解码后的对象后,仍需要进行验证 . 根据ID令牌验证的the guide,您必须检查以下内容:

    • exp 将来

    • iat 已过去

    • isshttps://securetoken.google.com/<firebaseProjectID>

    • aud<firebaseProjectID>

    • sub 非空

    因此,例如,您可以像这样检查 iss (其中 FIREBASE_APP_ID 是来自firebase控制台的应用程序ID):

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

    以下是刷新密钥和检索密钥的完整示例 .

    免责声明:我没有测试过,这是基本上仅供参考 .

    $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;
    }
    

    最好的事情是安排一个cronjob在需要时调用 checkKeys() ,但我不知道你的提供者是否允许这样做 . 而不是那样,你可以为每个请求执行此操作:

    checkKeys();
    $pkeys_raw = getKeys(); // check if $raw_keys is not null before using it!
    
  • 4

    接受答案的工作实例 . 注意差异:

    • 经过测试和工作

    • 适用于非类环境

    • 更多代码显示如何将其用于Firebase(简单,单行发送验证代码)

    • UnexpectedValueException涵盖了您可能看到的各种错误(例如过期/无效密钥)

    • 评论很好且易于理解

    • 从Firebase令牌返回一系列VERIFIED数据(您可以安全地将此数据用于您需要的任何内容)

    这基本上是一个破碎的,易于阅读/理解的PHP版本https://firebase.google.com/docs/auth/admin/verify-id-tokens

    注意:您可以使用getKeys(),refreshKeys(),checkKeys()函数生成用于任何安全api情况的密钥(使用您自己的模拟“verify_firebase_token”函数的功能) .

    使用:

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

    代码:

    $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;
    }
    
  • 5

    您可以查看此库,而不是手动完成所有操作:
    Firebase Tokens甚至Firebase Admin SDK for PHP . 缓存内容已经实现,只需看看文档即可 .

    基本上,您只需使用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();
        }
    

相关问题