ere*_*130 19 php android jwt firebase firebase-authentication
我有一个只有PHP(没有Java,没有node.js)的共享主机方案.我需要从我的Android应用程序发送firebase ID令牌并通过PHP-JWT验证它.
我正在按照教程:验证Firebase ID令牌
它说:
"如果您的后端使用的语言没有官方的Firebase Admin SDK,您仍然可以验证ID令牌.首先,找到适合您语言的第三方JWT库.然后,验证标题,有效负载和签名ID令牌."
我找到了这个库:Firebase-PHP-JWT.在gitHub示例中; 我无法理解
$关键部分:
`$key = "example_key";`
Run Code Online (Sandbox Code Playgroud)
和
$ token部分:
`$token = array(
"iss" => "http://example.org",
"aud" => "http://example.com",
"iat" => 1356999524,
"nbf" => 1357000000
);`
Run Code Online (Sandbox Code Playgroud)
我的问题:
编辑:
哦,我明白了.GitHub示例演示了如何生成JWT代码(编码)以及如何解码它.在我的情况下,我只需要解码由firebase编码的jwt.所以,我只需要使用这个代码:
$decoded = JWT::decode($jwt, $key, array('HS256'));
Run Code Online (Sandbox Code Playgroud)
在此代码部分中,$ 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 + 6kVdgZH4iKLi8gVjdcFfNsEpbOBoZqjipJ63l4A3mfxOkma0d2XgKR12KAfYX \ncAVPgihAPoNoUPJK0Nj + CmvNlUBXCrl9TtqGjK7AKi8 = \n ----- END CERTIFICATE ----- \n"
我是否需要在传递之前将此公钥转换为某些内容?我试图删除所有"\n"和"----- BEGIN CERTIFICATE -----","-----开始证书-----" ......但是没有运气.我仍然得到无效的签名错误.有什么建议?
Ger*_*ssy 18
仅当您使用密码对令牌进行签名时才使用HS256.Firebase在发出令牌时使用RS256,因此,您需要来自给定URL的公钥,并且您需要将算法设置为RS256.
另外请注意,您在您的应用程序获得令牌不应该是一个数组,但有3个部分的字符串:header
,body
,和signature
.每个部分用a分隔.
,因此它给你一个简单的字符串: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"]);
Run Code Online (Sandbox Code Playgroud)
现在$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;
Run Code Online (Sandbox Code Playgroud)
以下是刷新密钥和检索密钥的完整示例.
免责声明:我没有测试过,这基本上仅供参考.
$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;
}
Run Code Online (Sandbox Code Playgroud)
最好的事情是安排一个cronjob随时调用checkKeys()
,但我不知道你的提供商是否允许.而不是那样,你可以为每个请求执行此操作:
checkKeys();
$pkeys_raw = getKeys(); // check if $raw_keys is not null before using it!
Run Code Online (Sandbox Code Playgroud)
接受答案的工作实例.注意差异:
经过测试和工作
适用于非类环境
更多代码显示如何将其用于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>)
Run Code Online (Sandbox Code Playgroud)
代码:
$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;
}
Run Code Online (Sandbox Code Playgroud)
无需手动完成所有操作,您可以看一下以下库:
Firebase令牌甚至PHP的Firebase Admin SDK。缓存内容等已经实现,只需看一下文档即可。
基本上,您只需使用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();
}
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
8274 次 |
最近记录: |