DEV Community

Cover image for Connect to APNS via HTTP/2 with PHP
SaMauto
SaMauto

Posted on

Connect to APNS via HTTP/2 with PHP

Send a push message via APNs with a PHP Script

HTTP/2 is the new standard (and from November 2020 onwards the only way to connect to the APNS) in this article i’ll explain how to use PHP to connect to APNS using JWT token (recommended) or by using certificates.

Apple’s service to send push notifications to iPhones, iPads and even to Apple’s watches is called APNS: Apple Push Notification Service) .

In order to send Push Notifications you’ll need an IOS app and a server to send the message from. In this article I will provide information on how to send the messages via PHP using a library to make it easier to generate the required JWT token.

What do you need in order to send Push Notifications via APNS?

The delivery of remote notifications involves several key components. We’ll be needing a receiver (iPad, iPhone, ect) and a server to send the message from (provider server)

  • Your company’s/personal server (known as the provider server) -- we’ll be using PHP on our provider server
  • Apple Push Notification service (APNs) -- Private key (or certificate) from Apple (.p8 file)
  • The user’s (apple) device (iPad, iPhone, ect.)
  • IOS application (Your app running on the user’s device) -- for example: nl.samauto.ios-application

Getting the private key from Apple

Apple Push Notification service (APNs) must know the address of a user’s device before it can send notifications to that device. The address takes the form of a device token unique to both the device and your app ( nl.samauto.ios-application ) . At launch time, your app communicates with APNs and receives its device token, which you then forward to your provider server (the one we are going to build with PHP). Our server will include that token with any notifications it sends to Apple.

Head over to Developer.apple.com and login with your AppleId.

Go to “Certificates, Identifiers & Profiles > Keys“

Generate a new key for your application, make sure to enable the Push Notifications Capability.

Download the .p8 file and save it to a secure location.

IT CANNOT BE DOWNLOADED AGAIN (APPLE DELETES THE PRIVATE KEY)

Let's build the PHP Server!

We’ll be building our server with PHP.

To authenticate to APNS we are utilizing JWT (JSON Web Tokens – RFC 7519) to create a Token Based Authenticated connection to Apple’s servers.

We will use composer to add a package (jwt) developed by Luís Cobucci. lcobucci/jwt on GitHub or at packagist.org

To add the package (via composer) to the project use the following command:

composer require lcobucci/jwt

We can use the following script:

<?php

use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Lcobucci\JWT\Configuration;

$config = $container->get(Configuration::class);
assert($config instanceof Configuration);

$device_token = "device_token_here";
$apns_topic = 'to.dev.ios-application';
$p8file = "/home/dave/samauto/key_from_apple.p8";

$token = (string) $config->createBuilder()
->issuedBy("DEF123GHIJ") // (iss claim) // teamId
->issuedAt(time()) // time the token was issuedAt
->withHeader('kid', "ABC123DEFG")
->setKey('file://' . $p8file)
->setSigner(new Sha256()) // APNs only supports the ES256 algorithm
->getToken(); // get the generated token

$payloadArray['aps'] = [
  'alert' => [
    'title' => "Dev.To Push Notification", // title of the notification
    'body' => "Visit SamAuto.nl for more awesome scripts", // content/body of the notification
  ],
  'sound' => 'default',
  'badge' => 1
];

$payloadJSON = json_encode($payloadArray);

$url = "https://api.sandbox.push.apple.com/3/device/$device_token";
$ch = curl_init($url);

curl_setopt($ch, CURLOPT_POSTFIELDS, $payloadJSON);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token","apns-topic: $apns_topic"]);
$response = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// On successful response you should get true in the response and a status code of 200
// A list of responses and status codes is available at
// https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW1

var_dump($response);
var_dump($httpcode);

I will explain the script in steps.
First we'll need a deviceId from a IOS device, the application identifier of your application installed on the IOS device and the filePath to the .p8 file.

// replace the device token with "device_token_here"
$device_token = "device_token_here";

// use your IOS application ID
$apns_topic = 'to.dev.ios-application';

// replace $p8file with the location to your .p8 file you downloaded from Apple.
$p8file = "/home/dave/samauto/key_from_apple.p8";

Next we will use the JWT library to generate a JWT token to authenticate the HTTP/2 call to Apple's API.

// Replace "DEF123GHIJ" with your TeamId
// Replace "ABC123DEFG" with your (Encryption) KeyId

$token = (string) $config->createBuilder()
->issuedBy("DEF123GHIJ") // (iss claim) // teamId
->issuedAt(time()) // time the token was issuedAt
->withHeader('kid', "ABC123DEFG")
->setKey('file://' . $p8file)
->setSigner(new Sha256()) // APNs only supports the ES256 algorithm
->getToken(); // get the generated token

// $token will now contain a JWT Token for example:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

You can find out more about JWT tokens on JWT.IO

The next part of the script is creating a payload to send to apple and encode is as JSON.

$payloadArray['aps'] = [
  'alert' => [
    'title' => "Dev.To Push Notification", // title of the notification
    'body' => "Visit SamAuto.nl for more awesome scripts", // content/body of the notification
  ],
  'sound' => 'default',
  'badge' => 1
];

$payloadJSON = json_encode($payloadArray);

The payload will look like this:

{
    "aps": {
        "alert": {
            "title": "Dev.To Push Notification",
            "body": "Visit SamAuto.nl for more awesome scripts"
        },
        "sound": "default",
        "badge": 1
    }
}

We will send that to Apple's Sandbox URL (https://api.sandbox.push.apple.com/)
And append "3/device/" and the value of $device_token
Next we will build a cURL request (make sure you have a HTTP/2 enabled version of CURL installed)

curl_setopt($ch, CURLOPT_POSTFIELDS, $payloadJSON);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token","apns-topic: $apns_topic"]);
$response = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

If all went well you should see a 200 OK HTTP status code.

Happy Hacking!

Top comments (12)

Collapse
 
smillerdev profile image
Sean Molenaar

If you're trying to get this to work in 2021:
missing container usage can be replaced by

$config = Configuration::forUnsecuredSigner();
Enter fullscreen mode Exit fullscreen mode

And the builder signature changed to:


$token = (string) $config->builder()
                         ->issuedBy("teamId") // (iss claim) // teamId
                         ->issuedAt(new DateTimeImmutable()) // time the token was issuedAt
                         ->withHeader('kid', "keyId")
                         ->getToken(new Sha256(), new Key\LocalFileReference('file://' . $p8file)); // get the generated token
Enter fullscreen mode Exit fullscreen mode
Collapse
 
fodorbalint profile image
fodorbalint

Where does $container come from?

Collapse
 
gustavguez profile image
Gustavo Rodríguez

lcobucci-jwt.readthedocs.io/en/lat...

The examples here fetch the configuration object from a hypothetical dependency injection container. You can create it in the same script or require it from a different file. It basically depends on how your system is bootstrapped.

Collapse
 
deepak_panwar_1 profile image
Deepak Panwar

Just use GuzzleHttp\Client and we don't need JWT. See the working code-

$url = "api.sandbox.push.apple.com/3/device/";

$headers = array(
"apns-topic: com.example.exampleapp",
"apns-push-type: alert",
"Content-Type: application/x-www-form-urlencoded",
);

$certificate_file = "iosCertificates/apple-push-dev-certificate-with-key.pem";

$payloadArray['aps'] = [
'alert' => [
'title' => "Test Push Notification",
'body' => "Ohhh yeah working", ],
'sound' => 'default',
'badge' => 1

];

$data = json_encode($payloadArray);

$client = new Client();

$response = $client->post($url, [
'headers' => $headers,
'cert' => $certificate_file,
'curl' => [
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
],
'body'=> $data,

]);

Collapse
 
akebar__93923a3326cb958c1 profile image
Akebar

This is the correct answer. I was able to use this to send Safari Web Push Notifications.

Ensure that you include the device token in the URL:
$url = "api.sandbox.push.apple.com/3/devic...", otherwise it won't work.

Collapse
 
nishittops profile image
nishittops

I tries this. Getting following error:
Fatal error: Uncaught GuzzleHttp\Exception\ConnectException: cURL error 7: Failed to connect to api.sandbox.push.apple.com port 80: Connection timed out
Am I missing anything here?

Collapse
 
vipera177 profile image
Hemant Kumar

Those who are having the same issue just like I had. Add this code in the curl code and it will work.

        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
slarkjm0803 profile image
slarkjm0803

I am getting error with $container.

Collapse
 
samauto profile image
SaMauto

Don't forget there is a great package out there that can do this all: github.com/edamov/pushok

Collapse
 
imtiyaz profile image
Mohammed Imtiyaz

Where does the $container comes?

Collapse
 
randor profile image
Randy Mennie

Did anyone get an answer as to where $container comes from? I get no response whatsoever back from this script. I guess none of this actually works.

Collapse
 
vipera177 profile image
Hemant Kumar

I am getting 400 code on the server and 200 on my local end. How to fix it?