[英]How to verify firebase ID token with PHP(JWT)?
我有一個共享托管計划,其中只有 PHP(沒有 Java,沒有 node.js)。 我需要從我的 android 應用程序發送 firebase ID 令牌並通過 PHP-JWT 驗證它。
我正在學習教程:驗證 Firebase ID 令牌
它說:
如果您的后端使用的語言沒有官方 Firebase Admin SDK,您仍然可以驗證 ID 令牌。 首先,為您的語言找到第三方 JWT 庫。 然后,驗證 ID 令牌的標頭、有效負載和簽名。
我找到了那個庫: Firebase-PHP-JWT 。 在 gitHub 示例中; 我無法理解
$關鍵部分:
`$key = "example_key";`
和
$token 部分:
`$token = array(
"iss" => "http://example.org",
"aud" => "http://example.com",
"iat" => 1356999524,
"nbf" => 1357000000
);`
我的問題:
編輯:
好吧,我明白了。 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 庫來驗證簽名。 使用來自該端點的響應的 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+6kVdgZH4iKLi8gVjdcFfNsEpboOBoZqjipJ63l4A3mfxOkma0d2XgKR12KAfYX\ncAVPgihAPoNoUPJK0Nj+CmvNlUBXCrl9TtqGjK7AKi8=\n-----結束證書-----\n"
我需要在傳遞之前將此公鑰轉換為某種東西嗎? 我試圖刪除所有"\n"和"-----BEGIN CERTIFICATE-----" , "-----BEGIN CERTIFICATE-----" ...但沒有運氣。 我仍然收到無效簽名錯誤。 有什么建議嗎?
僅當您使用密碼對令牌進行簽名時才使用 HS256。 Firebase 在發布令牌時使用 RS256,因此,您需要來自給定 URL 的公鑰,並且需要將算法設置為 RS256。
另請注意,您在應用程序中獲得的令牌不應該是一個數組,而是一個包含 3 個部分的字符串: header
、 body
和signature
。 每個部分用 . 分隔.
,因此它為您提供了一個簡單的字符串: 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
在過去iss
https://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!
接受答案的工作示例。 注意差異:
經過測試和工作
在非課堂環境中工作
更多代碼顯示如何將其用於 Firebase(簡單、單行發送代碼進行驗證)
UnexpectedValueException 涵蓋了您可能會看到的各種錯誤(例如過期/無效的鍵)
評論很好,易於理解
從 Firebase 令牌返回一組 VERIFIED 數據(您可以安全地將此數據用於您需要的任何事情)
這基本上是https://firebase.google.com/docs/auth/admin/verify-id-tokens的一個完整的、易於閱讀/理解的 PHP 版本
注意:您可以使用 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;
}
您可以查看這個庫,而不是手動完成所有操作:
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();
}
如果有人仍然感興趣, @CFP Support的答案對於使用PHP 5.6
的服務器來說非常好,但是在嘗試緩存當前保存的公鑰的到期時間時確實存在一些錯誤。 我采用了該代碼,並進行了必要的更正:
composer.json
中的要求
{
"require" : {
"firebase/php-jwt": "5.2.0"
}
}
用法
<?
$verified = verify_firebase_token(<THE TOKEN FROM FIREBASE>);
?>
功能
<?
# 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;
}
?>
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.