Generating OAUTH token for firebase cloud messaging PHP
Asked Answered
D

5

14

I have a PHP page which i used to send notifications to the users of a mobile app i developed , this page works fine until last month , then it gave me this error

{"multicast_id":5174063503598899354,"success":0,"failure":1,"canonical_ids":0,"results":[{"error":"InvalidRegistration"}]}

i tried to generate OAUTH token using the documentation in this link https://firebase.google.com/docs/cloud-messaging/auth-server#node.js but it needs NODE.JS server and my server doesn't support Node.Js , i tried to use the Firebase Admin SDK but can't find anything. Here is the PHP code of the page

<?php

//Includes the file that contains your project's unique server key from the Firebase Console.
require_once("serverKeyInfo.php");

//Sets the serverKey variable to the googleServerKey variable in the serverKeyInfo.php script.
$serverKey = $googleServerKey;

//URL that we will send our message to for it to be processed by Firebase.
    $url = "https://fcm.googleapis.com/fcm/send";

//Recipient of the message. This can be a device token (to send to an individual device) 
//or a topic (to be sent to all devices subscribed to the specified topic).
$recipient = $_POST['rec'];

//Structure of our notification that will be displayed on the user's screen if the app is in the background.
$notification =array(
    'title'   => $_POST['title'],
    'body'   => $_POST['body'],
    'sound' => 'default'
);

//Structure of the data that will be sent with the message but not visible to the user.
//We can however use Unity to access this data.
$dataPayload =array( 

    "powerLevel" => "9001",
    "dataString" => "This is some string data"
);

//Full structure of message inculding target device(s), notification, and data.
$fields =array(

    'to'  => $recipient,
    'notification' => $notification,
    'data' => $dataPayload
);

//Set the appropriate headers
$headers = array(

'Authorization: key=' . $serverKey,
'Content-Type: application/json'
);
//Send the message using cURL.
$ch = curl_init();
curl_setopt( $ch,CURLOPT_URL, $url);
curl_setopt( $ch,CURLOPT_POST, true );
curl_setopt( $ch,CURLOPT_HTTPHEADER, $headers );
curl_setopt( $ch,CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch,CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch,CURLOPT_POSTFIELDS, json_encode( $fields ) );
$result = curl_exec($ch );
curl_close( $ch );

//Result is printed to screen.
echo $result;
?>

Can anyone send me an example o how can i do this ( I am beginner in PHP ) Thanks in advance

*Update : Also i tried to change the $url in the code to

$url = "https://fcm.googleapis.com/v1/projects/notifications-9ccdd/messages:send";

but it gives me this error

"error": { "code": 401, "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", "status": "UNAUTHENTICATED" } Blockquote

Discord answered 8/1, 2021 at 16:53 Comment(0)
H
31

For anyone looking for an answer without the use of external libraries or packages. This code is based on this documentation from google: https://developers.google.com/identity/protocols/oauth2/service-account#httprest

Step 1: In your http://console.firebase.google.com under project->settings->service accounts locate the Firebase service account. Generate a new private key and download the json file.

// This function is needed, because php doesn't have support for base64UrlEncoded strings
function base64UrlEncode($text)
{
    return str_replace(
        ['+', '/', '='],
        ['-', '_', ''],
        base64_encode($text)
    );
}

// Read service account details
$authConfigString = file_get_contents("path_to_the_json_file_you_just_downloaded.json");

// Parse service account details
$authConfig = json_decode($authConfigString);

// Read private key from service account details
$secret = openssl_get_privatekey($authConfig->private_key);

// Create the token header
$header = json_encode([
    'typ' => 'JWT',
    'alg' => 'RS256'
]);

// Get seconds since 1 January 1970
$time = time();

// Allow 1 minute time deviation between client en server (not sure if this is necessary)
$start = $time - 60;
$end = $start + 3600;

// Create payload
$payload = json_encode([
    "iss" => $authConfig->client_email,
    "scope" => "https://www.googleapis.com/auth/firebase.messaging",
    "aud" => "https://oauth2.googleapis.com/token",
    "exp" => $end,
    "iat" => $start
]);

// Encode Header
$base64UrlHeader = base64UrlEncode($header);

// Encode Payload
$base64UrlPayload = base64UrlEncode($payload);

// Create Signature Hash
$result = openssl_sign($base64UrlHeader . "." . $base64UrlPayload, $signature, $secret, OPENSSL_ALGO_SHA256);

// Encode Signature to Base64Url String
$base64UrlSignature = base64UrlEncode($signature);

// Create JWT
$jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;

//-----Request token, with an http post request------
$options = array('http' => array(
    'method'  => 'POST',
    'content' => 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion='.$jwt,
    'header'  => "Content-Type: application/x-www-form-urlencoded"
));
$context  = stream_context_create($options);
$responseText = file_get_contents("https://oauth2.googleapis.com/token", false, $context);

$response = json_decode($responseText);

The response has 3 fields:

  • access_token
  • expires_in: the number of seconds before the token expires, eg 3599 (3600s = 1h)
  • token_type: "Bearer"

Use the access_token in the calls to firebase to authenticate your call. You should also store this access_token together with the expires_in, and request a new token when it almost expires. The maximum lifetime of a token is 1 hour.

Heyer answered 17/2, 2023 at 9:43 Comment(8)
Superb answer! it works like a charm. I have a few question though, the $result variable seems to be not used or was it supposedly to be used for $base64UrlSignature? Also is it ok to create 10-20 access token in a single minute or should i just save it somewhere to be retrieved and just generate a new one after it expires?Burn
$result can be used to check if the signing was successful. You should store the access token somewhere, and only request a new one when the old one almost expires, according to Google. It's probably rate limited.Heyer
Much simpler than every other implementation I found, all of which require a bunch of dependencies. For whatever reason the POST with file_get_contents didn't work for me, but it's fine after switching it to CURL.Koren
You saved me hours of work.Tapetum
thank you for your solving my problem with above code, I can sent notification to one token device but I can not send to multiple devices. this is error : description: "Invalid JSON payload received. Unknown name "token" at 'message': Proto field is not repeating, cannot start list." could you help me please?Hyacinthia
@VahitBehrouz With the new API it's not possible to send notifications to multiple device tokens. You'll need to make individual API requests for each device token, or use topic to broadcast to multiple devices.Heyer
@PeterB: thank you for your big guide me, could you write me or link me how can I do that with useing topic to broadcast? Thank you so much.Hyacinthia
@VahitBehrouz Ask a new question if necessary. But basically when your app registers for notifications, it can register to one or more topics. Then when sending notifications, you can specify for which topic(s) it is. So you do not need to send notifications to specific device IDs.Siobhan
L
23

****** 2023 UPDATE ****** FCM http Legacy has been officially deprecated, and will be removed entirely by June 2024. Anybody using the http Legacy version should migrate to V1 instead. The example below uses V1 and not the http Legacy version :)


For anybody still looking for an answer to this (2021), in order to send a push message via your own PHP system to the Firebase messaging system you need an access token from Google Credentials. Here's how to do it - please note I've only done this in PHP Laravel, not raw PHP. But you should be able to locate the vanilla PHP solution to this by modifying the steps to suit (Also same with Code Igniter and other PHP libraries)

  1. In your http://console.firebase.google.com under project->settings->service accounts locate the Firebase service account. Generate a new private key and download the json file. Store it on your server somewhere users can't get to it.

  2. Install Google API Client. For Laravel this is:

      composer require google/apiclient --with-all-dependencies
    
  3. Open composer.json, and add to the autoload array. For Laravel this is:

     "classmap": [
         "vendor/google/apiclient/src/Google"
     ],
    
  4. Create a new Service class (or create a new Class if vanilla PHP), and add the following method for retrieving an access token:

    private function getGoogleAccessToken(){
    
         $credentialsFilePath = 'the-folder-and-filename-of-your-downloaded-service-account-file.json'; //replace this with your actual path and file name
         $client = new \Google_Client();
         $client->setAuthConfig($credentialsFilePath);
         $client->addScope('https://www.googleapis.com/auth/firebase.messaging');
         $client->refreshTokenWithAssertion();
         $token = $client->getAccessToken();
         return $token['access_token'];
    }
    
  5. Now create a method to send all your message info to Firebase via CURL:

    public function sendMessage(){
    
     $apiurl = 'https://fcm.googleapis.com/v1/projects/your-project-id/messages:send';   //replace "your-project-id" with...your project ID
    
     $headers = [
             'Authorization: Bearer ' . $this->getGoogleAccessToken(),
             'Content-Type: application/json'
     ];
    
     $notification_tray = [
             'title'             => "Some title",
             'body'              => "Some content",
         ];
    
     $in_app_module = [
             "title"          => "Some data title (optional)",
             "body"           => "Some data body (optional)",
         ];
     //The $in_app_module array above can be empty - I use this to send variables in to my app when it is opened, so the user sees a popup module with the message additional to the generic task tray notification.
    
      $message = [
            'message' => [
                 'notification'     => $notification_tray,
                 'data'             => $in_app_module,
             ],
      ];
    
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, $apiurl);
      curl_setopt($ch, CURLOPT_POST, true);
      curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
      curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($message));
    
      $result = curl_exec($ch);
    
      if ($result === FALSE) {
          //Failed
          die('Curl failed: ' . curl_error($ch));
      }
    
      curl_close($ch);
    
    }
    

Google recommends that you only use this method if you can't add the JSON file as an environmental variable on your server directly. I don't know why Google doesn't have better documentation on this subject for PHP, it seems to prefer node.js , Go, Java and C++.

Lionhearted answered 24/8, 2021 at 14:3 Comment(6)
I tried this exact method suggested here but my access tokens contains a lot of '....' please help me outPenumbra
@YeoBryan I can't tell exactly but I would assume something hasn't been set up correctly in firebase, or the private key is wrong, or it can't find your json file. The php code should be working.Lionhearted
if possible could you please take a look at the question I posted and offer any potential solution? I have been stuck for hours not sure what I am doing wrongly #72271418Penumbra
I have the service account json file downloaded from firebase and placed into my PHP project. I think its able to find the file because its able to return results to me (#72271418)Penumbra
Though I can't find it referenced anywhere, and no one seems to know of it's existence...there is an official fcm client for sending messages rather than doing your own cURL requests github.com/googleapis/google-api-php-client-services/tree/main/…Jingoism
Thanks for the update. Does this mean we no longer have to use our local json service account file?Lionhearted
R
8

@james have provided a very useful and clear answer. You can follow his guide or For pure php Simply

  1. In your http://console.firebase.google.com under project->settings->service accounts locate the Firebase service account. Generate a new private key and download the json file. Upload to a location on your server
  2. Install Google API Client: composer require google/apiclient
  3. In your PHP file
require "./vendor/autoload.php";
$client= new Google_Client();
$client->setAuthConfig("path-downloaded-json-file.json");
$client->addScope('https://www.googleapis.com/auth/firebase.messaging');
$client->refreshTokenWithAssertion();
$token = $client->getAccessToken();
echo $token['access_token'];
Revest answered 30/10, 2021 at 6:45 Comment(1)
I tried this exact method suggested here but my access tokens contains a lot of '....' please help me outPenumbra
W
0

First you need JWT!

You can find here Link for JWT GitHub

Creating access token:

use Firebase\JWT\JWT;
use Firebase\JWT\Key;


require 'vendors/php-jwt-main/src/JWT.php';
require 'vendors/php-jwt-main/src/Key.php';

function returnFireBaseTkn(){
    $jsonInfo = json_decode(file_get_contents("YOUR JSON FILE WITH CREDENTIAL HERE"), true);

    $now_seconds = time();
    
    $privateKey = $jsonInfo['private_key'];
    
    $payload = [
        'iss' => $jsonInfo['client_email'],
        'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
        'aud' => $jsonInfo['token_uri'],
        //Token to be expired after 1 hour
        'exp' => $now_seconds + (60 * 60),
        'iat' => $now_seconds
    ];
    
    $jwt = JWT::encode($payload, $privateKey, 'RS256');
    
    // create curl resource
    $ch = curl_init();
    
    // set post fields
    $post = [
        'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion' => $jwt
    ];
    
    $ch = curl_init($jsonInfo['token_uri']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
    
    // execute!
    $response = curl_exec($ch);
    
    // close the connection, release resources used
    curl_close($ch);
    
    // do anything you want with your response
    $jsonObj = json_decode($response, true);

    return $jsonObj['access_token'];
}

Sending notification:

function sendNotif($info){

    $apiurl = 'https://fcm.googleapis.com/v1/projects/your-project-id/messages:send';

    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $apiurl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($info));

    $headers = array(
    //It's normal to find .............................. on the access token!!
        'Authorization: Bearer ' . 'access_token_ID_here',
        'Content-Type: application/json'
    );

    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $result = curl_exec($ch);

    if (curl_errno($ch)) {
        echo 'Error:' . curl_error($ch);
    }
    curl_close($ch);
}

// Notification need to be in this format for single user!
$notification = array(
    "message" => array(
        "token" => "USER_ID",
        "notification" => array(
            "title" => "teste",
            "body" => "123"
        )
    )
);

sendNotif($notification);

If you want to use for a group of users:

$notification = array(
    "message" => array(
        "topic" => "GROUP_ID",
        "notification" => array(
            "title" => "teste",
            "body" => "123"
        )
    )
);
Week answered 8/7, 2022 at 19:52 Comment(0)
A
0

For laravel run the following

sudo composer require google/apiclient

In the Model create a function called getBearerToken that stores the token for 55 min in cache. you can change the time, but in my case token lifetime was 1 hr.

use Google\Client as GoogleClient;
use Carbon\Carbon;

private function getBearerToken(){
    try{
        $cache_key  = md5('google-auth-cache');
        $token      = \Cache::get($cache_key); 
        if(empty($token)){
            $serviceAccountPath = 'path/to/google/service-account.json';                  
            $client = new GoogleClient();
            $client->setAuthConfig($serviceAccountPath);
            $client->addScope('https://www.googleapis.com/auth/firebase.messaging');
            $client->fetchAccessTokenWithAssertion();
            $token = $client->getAccessToken();
            $expiresAt  = Carbon::now()->addMinutes(55);
            \Cache::put($cache_key, $token, $expiresAt); 
        }
        $accesstoken = isset($token['access_token']) ? $token['access_token'] : '';

        return $accesstoken;
    }
    catch(\Exception $e){
        echo sprintf('Line : %s, Message : %s', $e->getLine(), $e->getMessage());
    }
}    

Now you can use this function for sending notification

$token = self::getBearerToken();
$headers = array("Authorization: Bearer $token", "Content-Type:application/json");
Already answered 29/6 at 19:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.