web-dev-qa-db-ja.com

firebase IDトークンをPHP(JWT)で検証する方法は?

PHPのみ(Javaなし、node.jsなし)の共有ホスティングプランがあります。 Firebase IDトークンをAndroidアプリから送信し、PHP-JWTで検証する必要があります。

私はチュートリアルに従っています: Firebase IDトークンの確認

それは言います:

「バックエンドが公式のFirebase Admin SDKを持たない言語である場合でも、IDトークンを検証できます。まず、その言語のサードパーティJWTライブラリを見つけます。次に、ヘッダー、ペイロード、および署名を検証しますIDトークン。」

そのライブラリを見つけました: Firebase-PHP-JWT 。 gitHubの例;理解できませんでした

$ keyパーツ:

`$key = "example_key";` 

そして

$トークン部分:

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

私の質問:

  1. $ key変数は何ですか?
  2. &token変数が配列である理由モバイルアプリから送信されるトークンは文字列です。
  3. 誰かがPHP-JWTでfirebase IDを検証する完全な例を投稿できるなら、感謝します。

編集:

わかりました、私はポイントを得ました。 GitHubの例は、JWTコード(エンコード)の生成方法とデコード方法を示しています。私の場合、firebaseによってエンコードされたjwtをデコードするだけです。だから、私はこのコードのみを使用する必要があります:

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

このコード部分では$ jwtはfirebase IDトークンです。 For$ key変数ドキュメントには次のように書かれています:

最後に、IDトークンが、トークンの子供の要求に対応する秘密キーによって署名されていることを確認します。 https://www.googleapis.com/robot/v1/metadata/x509/[email protected] から公開鍵を取得し、JWTライブラリを使用して署名を検証します。そのエンドポイントからの応答のCache-Controlヘッダーでmax-ageの値を使用して、公開キーをいつ更新するかを確認します。

この公開キーをデコード機能に渡す方法を理解できませんでした。キーは次のようなものです:

「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」は

この公開鍵を渡す前に何かに変換する必要がありますか?すべてを削除しようとしました "\ n"および "-----証明書の開始----- 「」----- BEGIN CERTIFICATE ----- "...しかし運はありません。それでも、無効な署名エラーが発生します。何かアドバイス?

19
eren130

HS256は、パスワードを使用してトークンに署名する場合にのみ使用されます。 Firebaseはトークンを発行するときにRS256を使用するため、指定されたURLの公開キーが必要であり、アルゴリズムをRS256に設定する必要があります。

また、アプリケーションで取得するトークンは、配列ではなく、headerbody、およびsignatureの3つの部分からなる文字列でなければなりません。各部分は.で区切られているため、単純な文字列header.body.signatureが得られます。

トークンを検証するために必要なことは、公開鍵を 所定のURL から定期的にダウンロードし(その情報についてはCache-Controlヘッダーを確認して)、ファイルに保存することです(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トークンの検証によると、次のことを確認する必要があります。

  • 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/[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;
}

一番良いのは、必要なときにcheckKeys()を呼び出すcronjobをスケジュールすることですが、プロバイダーがそれを許可しているかどうかはわかりません。その代わりに、すべてのリクエストに対してこれを行うことができます:

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

すべてを手動で行う代わりに、このライブラリを見ることができます。
Firebase Tokens または Firebase Admin SDK for PHP 。キャッシングなどは既に実装されています。ドキュメントをご覧ください。

基本的に、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();
    }
5
baris1892

受け入れられた答えの実例。注の違い:

  • テスト済みおよび動作中

  • 非クラス環境で動作します

  • Firebaseでの使用方法を示すより多くのコード(検証のためにコードを送信するための単純な1行のライナー)

  • UnexpectedValueExceptionは、表示される可能性のあるあらゆる種類のエラー(期限切れ/無効なキーなど)をカバーします

  • コメントがよくわかりやすい

  • firebase Tokenから検証済みデータの配列を返します(必要に応じてこのデータを安全に使用できます)

これは基本的に壊れた、読みやすい/わかりやすい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/[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