У меня есть общий хостинг-план, который имеет только PHP (без Java, no node.js). Мне нужно отправить токен идентификатора firebase из моего приложения для Android и проверить его с помощью PHP-JWT.
Я следую инструкциям: Проверить токены Firebase ID
В нем говорится:
«Если ваш бэкэнд на языке, который не имеет официального пакета Firebase Admin SDK, вы все равно можете проверить токены идентификатора. Сначала найдите стороннюю библиотеку JWT для своего языка. Затем проверьте заголовок, полезную нагрузку и подпись маркер идентификатора. "
Я нашел эту библиотеку: Firebase-PHP-JWT . В примере gitHub; я не мог понять
$ ключевая часть:
`$key = "example_key";`
а также
$ token часть:
`$token = array( "iss" => "http://example.org", "aud" => "http://example.com", "iat" => 1356999524, "nbf" => 1357000000 );`
Мои вопросы:
РЕДАКТИРОВАТЬ:
Okey, я понял. Пример GitHub показывает, как генерировать код JWT (кодировать) и как его декодировать. В моем случае мне нужно только декодировать jwt, который закодирован firebase. Итак, мне нужно использовать только этот код:
$decoded = JWT::decode($jwt, $key, array('HS256'));
В этой части кода $ jwt является токеном идентификатора firebase . Для $ key variable документации говорится:
Наконец, убедитесь, что токен идентификатора был подписан закрытым ключом, соответствующим заявке ребенка на токен. Возьмите открытый ключ с https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com и используйте библиотеку JWT для проверки подписи. Используйте значение max-age в заголовке Cache-Control ответа от этой конечной точки, чтобы узнать, когда обновлять открытые ключи.
Я не понял, как передать эти открытые ключи для декодирования функции. Ключи:
«—– BEGIN CERTIFICATE —– \ nMIIDHDCCAgSgAwIBAgIIZ36AHgMyvnQwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE \ nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTcw \ nMjA4MDA0NTI2WhcNMTcwMjExMDExNTI2WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl \ nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD \ nggEPADCCAQoCggEBANBNTpiQplOYizNeLbs + r941T392wiuMWr1gSJEVykFyj7fe \ nCCIhS / zrmG9jxVMK905KwceO / FNB4SK + l8GYLb559xZeJ6MFJ7QmRfL7Fjkq7GHS \ n0 / sOFpjX7vfKjxH5oT65Fb1 + Hb4RzdoAjx0zRHkDIHIMiRzV0nYleplqLJXOAc6E \ n5HQros8iLdf + ASdqaN0hS0nU5aa / ПРОЦЕССОРЫ / EHQwfbEgYraZLyn5NtH8SPKIwZIeM7Fr \ ИПВ + 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 —–» , «—– BEGIN CERTIFICATE —–» … Но не повезло. Тем не менее я получаю недопустимую ошибку подписи. Любой совет?
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"]);
Теперь переменная $decoded
содержит полезную нагрузку маркера. Когда у вас есть декодированный объект, вам все равно нужно его проверить. Согласно руководству по проверке токена ID, вы должны проверить следующее:
exp
в будущем iat
в прошлом iss
: https://securetoken.google.com/<firebaseProjectID>
aud
: <firebaseProjectID>
sub
непусто Так, например, вы можете проверить это как это (где FIREBASE_APP_ID
– это идентификатор приложения из консоли firebase):
$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 (ie 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!
Рабочий пример https://stackoverflow.com/users/2644098/gergely-k%c5%91r%c3%b6ssy отличный ответ.
различия в примечании:
Протестировано и работает
работает в неклассических средах
Дополнительный код, показывающий, как использовать его для Firebase (простой, однострочный, чтобы отправить код для проверки)
UnexpectedValueException охватывает всевозможные ошибки, которые вы можете увидеть (например, истекшие / недействительные ключи)
Хорошо прокомментированный и простой в использовании
возвращает массив данных VERIFIED от Token Firebase (вы можете безопасно использовать эти данные для чего-то, что вам нужно)
ОБЛАСТЬ ПРИМЕНЕНИЯ:
$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>; function get_userId_by_deviceToken($subToken) { //// CONNECT TO YOUR DB //// RETURN THE USERID THAT MATCHES THE DEVICE/USER TOKEN } /////// 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 = false; if (!empty($decoded->sub)) { $userId = get_userId_by_deviceToken($decoded->sub); $sub = !empty($userId); } if ($exp && $iat && $aud && $iss && $sub) { // we have a confirmed Firebase user! // build an array with data we need for further processing $return['userId'] = $userId; $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 (ie 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; }