До июня 2024 года API Firebase использовал постоянный токен доступа, который можно
было получить один раз в консоли. Однако с теперь необходимо
запрашивать новый токен каждый час. Такой подход повышает безопасность и защищает
от возможной компрометации ключей.
PHP класс для получения токена API Firebase (HTTP v1)
Данная заметка служит вспомогательным материалом к статье:
Настройка push-уведомлений в приложении Apache Cordova .
Также она используется для создания модуля 1С-Битрикс в видеоруке: Создание модуля для 1С-Битрикс, часть 2
Перейдите в консоль Firebase и скачайте JSON-файл с доступами к API. Он находится в разделе Project Settings → Service Accounts. В результате вы получите файл вида:
{
"type": "service_account",
"project_id": "ru-bxstore",
"private_key_id": "e66ae616565f7dd332c18b7b",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEoibl1N0w=\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@ru-tech-bxstore.iam.gserviceaccount.com",
"client_id": "107969483695",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40ru.iam.gserviceunt.com",
"universe_domain": "googleapis.com"
}
После этого добавьте и подключите нижеописанный класс на странице, где планируете использовать токен.
В рамках видеоруководства создан модуль, содержащий этот класс. Его можно скачать по ссылке, приведенной в видеоуроке.
Класс максимально прокомментирован, можете использовать не только в 1С-Битрикс но в любом PHP приложении (не забудте скорректировать namespace):
namespace mibazarow\pushsender;
class GetFirebaseToken
{
// Приватное свойство для хранения приватного ключа из JSON
protected $privateKey;
// Приватное свойство для хранения email клиента из JSON
protected $clientEmail;
/**
* Конструктор: принимает либо путь к файлу JSON, либо строку JSON
* @param string $jsonInput - путь к файлу или JSON-строка
*/
public function __construct(string $jsonInput)
{
// Проверяем, существует ли файл по пути $jsonInput
if (file_exists($jsonInput)) {
// Если файл есть, читаем его содержимое
$content = file_get_contents($jsonInput);
// Декодируем содержимое JSON в массив
$data = json_decode($content, true);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Ошибка при декодировании JSON из файла: ' . json_last_error_msg());
}
} else {
// Иначе предполагаем, что входная строка — JSON
$data = json_decode($jsonInput, true);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Ошибка при декодировании JSON строки: ' . json_last_error_msg());
}
}
// Проверяем наличие обязательных полей: private_key и client_email
if (!isset($data['private_key']) || !isset($data['client_email'])) {
throw new \Exception('Недостающие поля в JSON: private_key или client_email');
}
// Сохраняем полученные данные в свойства класса
$this->privateKey = $data['private_key'];
$this->clientEmail = $data['client_email'];
}
/**
* Получение токена доступа Firebase
* @param int|null $AccessTokenMaximumExpirationTime - максимально допустимое время жизни токена (секунд), по умолчанию 3600
* @param int|null $RequestTimeout - таймаут запроса curl (секунд), по умолчанию 10
* @return string - токен доступа
*/
public function getAccessToken(?int $AccessTokenMaximumExpirationTime = 3600, ?int $RequestTimeout = 10): string
{
// Вызов статического метода для получения токена
return self::requestAccessToken($this->privateKey, $this->clientEmail, $AccessTokenMaximumExpirationTime, $RequestTimeout);
}
/**
* Статический метод для запроса access_token из Firebase с помощью JWT
* @param string $PrivateKey - приватный ключ в формате PEM
* @param string $AccountName - email учетной записи (client_email)
* @param int|null $AccessTokenMaximumExpirationTime - время жизни токена (сек)
* @param int|null $RequestTimeout - таймаут curl (сек)
* @return string - токен доступа
*/
public static function requestAccessToken(
string $PrivateKey,
string $AccountName,
?int $AccessTokenMaximumExpirationTime = 3600,
?int $RequestTimeout = 10
): string {
// Проверка наличия приватного ключа и имени аккаунта
if ($PrivateKey !== '' && $AccountName !== '') {
// Время начала выполнения запроса
$StartTime = time();
// Создаем заголовок JWT (алгоритм RS256, тип JWT)
$Header = json_encode([
'alg' => 'RS256',
'typ' => 'JWT'
]);
// Проверка и установка значения таймаута
if (!isset($RequestTimeout) || $RequestTimeout < 0) $RequestTimeout = 10;
// Ограничение времени жизни токена, максимум 3600 секунд
if (!isset($AccessTokenMaximumExpirationTime) || $AccessTokenMaximumExpirationTime <= 0 || $AccessTokenMaximumExpirationTime > 3600) {
$AccessTokenMaximumExpirationTime = 3600;
}
// Рассчет времени окончания действия токена
$AccessTokenExpirationEndTime = $StartTime + $AccessTokenMaximumExpirationTime;
// URL аудитории (Firebase OAuth2 endpoint)
$Aud = 'https://oauth2.googleapis.com/token';
// Создаем тело JWT (заявку) с нужными данными
$Data = json_encode([
'iss' => $AccountName, // issuer — email аккаунта
'scope' => 'https://www.googleapis.com/auth/firebase.messaging', // область
'aud' => $Aud, // аудитория
'exp' => $AccessTokenExpirationEndTime, // время истечения
'iat' => $StartTime // время выпуска
]);
// Кодируем заголовок и данные в base64url
$Base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($Header));
$Base64UrlData = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($Data));
// Подписываем данные приватным ключом
$Signature = '';
$PrivateKeyResource = openssl_get_privatekey($PrivateKey);
openssl_sign("{$Base64UrlHeader}.{$Base64UrlData}", $Signature, $PrivateKeyResource, OPENSSL_ALGO_SHA256);
// Кодируем подпись в base64url
$Base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($Signature));
// Формируем JWT
$JWT = "{$Base64UrlHeader}.{$Base64UrlData}.{$Base64UrlSignature}";
// Подготовка данных для POST-запроса
$PostFields = [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $JWT
];
// Инициализация curl-запроса
$Ch = curl_init($Aud);
curl_setopt($Ch, CURLOPT_CONNECTTIMEOUT, $RequestTimeout);
curl_setopt($Ch, CURLOPT_TIMEOUT, $RequestTimeout);
curl_setopt($Ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($Ch, CURLOPT_POSTFIELDS, http_build_query($PostFields));
// Выполняем запрос и получаем ответ
$Result = curl_exec($Ch);
$Errno = 0;
$Error = '';
if ($Result === false) {
// В случае ошибки curl сохраняем ошибку
$Errno = curl_errno($Ch);
$Error = curl_error($Ch);
}
// Закрываем curl-сессию
curl_close($Ch);
// Обработка ошибок curl
if ($Errno > 0 && $Error != '') {
throw new \Exception($Error, $Errno);
} else {
if ($Result !== false) {
// Декодируем ответ JSON
$ResultArray = @json_decode($Result, true);
// Проверяем наличие access_token
if (is_array($ResultArray)) {
if (isset($ResultArray['access_token'])) {
// Возвращаем полученный токен
return trim($ResultArray['access_token']);
} else if (isset($ResultArray['error'])) {
// Обрабатываем ошибку из API
throw new \Exception($ResultArray['error']['message'], $ResultArray['error']['code']);
}
}
}
}
} else {
// Проверяем наличие обязательных данных и выбрасываем исключения
if ($PrivateKey == '') {
throw new \Exception('Не указан закрытый ключ.', 110);
}
if ($AccountName == '') {
throw new \Exception('Не указано имя учетной записи.', 110);
}
}
// Возвращаем пустую строку, если что-то пошло не так
return '';
}
}
Использование класса:
use mibazarow\pushsender\GetFirebaseToken;
// Указание пути к файлу JSON
$firebase = new GetFirebaseToken('/ПУТЬ_К_ФАЙЛУ/ВАШ.json');
// Или создание объекта с помощью строки JSON
$jsonString = 'Здесь ваша JSON строка';
$firebase = new GetFirebaseToken($jsonString);
// Получение токена
$token = $firebase->getAccessToken();
// Ваш токен, можно использовать
echo $token;
Обратите внимание: Используйте либо полный путь к файлу JSON, либо передавайте строку с его содержимым.
В рамках создания модуля осуществляется чтение JSON из настроек модуля, сохраненных в базе данных. Поэтому в коде JSON получается в виде строки и передается в конструктор класса именно так.