When working on a project recently, I needed an easy solution for testing and debugging push notifications. So I wrote a Swift command-line tool allowing me to send notifications directly from the terminal: lola. I’d like to share what I learned.

I’ll explain how to create a token for APNs and send notification from the terminal using Swift. This is not a step-by-step tutorial for full implementation. I’ll just be going over the most complicated part of the implementation process. If you want to look at the working code, you can check the terminal app lola.

Example of lola usage

APNs Request

Sending a notification is triggered by a POST request to the APNs. The request needs to be verified (otherwise anyone could send you a notification). There are two ways of doing that:

The first option is via a certificate-based connection. This requires establishing a connection with the server using a PEM certificate. 

The second option is newer and (IMHO) a little bit easier. You can use token-based authentication. It’s done by adding a specific token to the request headers.

APNs Token

The token contains three parts:  headher.payload.signature.

So the result can look something like this:

eyJhbGciOiJFUzI1NiIsImtpZCI6IkpQOFo3WFhLRDkifQ.eyJpc3MiOiI5UTY5MjI3NDJZIiwiaWF0IjoxNTc4NjQ1MDY4LCJleHAiOjE1Nzg2NDg2Njh9.88edmbUGRsFJzflh-JwIa4cc6omPSN7YwXzgknv-LGJv6hCQKE1kzoSQMl0d5rRk90BUMn9FHujO5GYbYBPj7w

For signing the token, you first need to create and download a private key from the Apple developer portal – keys section. Don’t forget to select Apple Push Notifications service (APNs). The key will be available for downloading only once, so make sure to save it. 

When you lose your P8 file (or someone stole it), you should create a new key and revoke the old key in the Apple developer portal. 

Header part

The header is a base64 encoded JSON with two values:

{
 "alg": "ES256",
 "kid": String
}

The first value, alg, is the encryption algorithm used. APNs supports only ES256, so this value is constant for us. The second value, kid, is your P8 key ID.

Tip: If you use lola to send notifications from the terminal, kid is parsed from the P8 key file name, so don’t rename it.

Payload part

Like the header, the payload is a base64 encoded JSON, too:

{
 "iss": String
 "iat": Int
 "exp": Int
}

iss is your Team ID. You can find it in your Account under Membership. The second required parameter, iat, is the issue date and should be set on the current Unix timestamp. The last parameter, exp, is optional. It’s the time when your token will expire; usually, it is good to set it for one hour.

Signature

When you open your P8 file in the text editor, you can see it contains a private key in the ANS1 format. You can read it using Apple’s CommonCrypto framework function SecKeyCreateWithData.

SecKeyCreateWithData(self as CFData,
                    [kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, // 1
                     kSecAttrKeyClass: kSecAttrKeyClassPrivate, // 2
                     kSecAttrKeySizeInBits: 256] as CFDictionary, // 3
                    &error)

To  correctly read your data, you must set up a CFDictionary for your current private key:

  1. Define your key as Elliptic Curve Digital Signature Algorithm (ECDSA)
  2. Specify it is a private key
  3. Define curve as P-256

When you have your SecKey, you can create a signature for any data:

let signature = SecKeyCreateSignature(self, algorithm, digestData as CFData, &error)

The algorithm for this example should be:

SecKeyAlgorithm.ecdsaSignatureDigestX962SHA256

This allows you to sign your header.payload. In this example, a hash message with SHA256 is required.

var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
CC_SHA256((message as NSData).bytes, CC_LONG(message.count), &hash)
let digestData = Data(bytes: hash)

When you join your header with the payload and the signature (headher.payload.signature), you will finally get your token for the APNs.

Create a URLRequest

Two more parameters are required for the headers:

apns-topic — this is the bundleId of your app.

apns-push-type — this is one of six different types of Apple notifications (alert, background, voip, complication, fileprovider and mdm).

Don’t forget to set httpMethod to "POST".

The last thing you have to do is make sure the request body contains a valid JSON.  You can use the following simple notification payload for testing purposes. To see all the things the JSON can include look at payload documentation.

{
  "aps": {
    "alert": "Test"
  }
}

Sending the Request

You can send your request to two URLs: 

  • Production — api.push.apple.com
  • Development — api.sandbox.push.apple.com

Regardless of which URL you choose, define your port as 443 (if you have a firewall issue, you can use 2197).

The request should be sent to the /3/device/deviceToken endpoint. The deviceToken should be the token or your device.

Lola

If you need more information about the implementation process, check out our open source project for sending push notifications from the terminal: lola

The example usage of lola:

$ lola  \
-bundleId co.industrial-binaries.LolaTestApp  \
-device d9f1767bdbf0371f5efb25c7873f1942cf570ececde9896913ed9fdb33ac1c26  \
-teamId 9Q6922742Y \
-authKey AuthKey_JP8Z7XXKD9.p8  \
-json  "{ \"aps\": {\"alert\": \"Test\", \"sound\": \"default\" }}"

Join our newsletter