Token Based Authentication and HTTP/2 Example with APNS

Update Dec 5, 2016: I've packaged a library for interacting with APNs using the methods outlined below which is available via pip. https://github.com/genesluder/python-apns

No more fiddling with push notification certificates! At last, Apple offers token-based authentication with the Apple Push Notification Service, greatly simplifying push server maintenance.

You simply generate a key once in the member center and use that key to generate authentication tokens on your server. You never need to recreate the key unless you chose to- for instance if you ever have reason to believe your key has been compromised.

Below I'll walk though how to generate an authentication token and send a push notification to the HTTP/2 service in Python. A full example is included at the end.

Install requirements

APNS uses JSON web tokens, so grab the Python library, PyJWT

pip install pyjwt

You'll also need the optional PyJWT dependency cryptography to support ECDSA algorithms

pip install cryptography

We need to use the HTTP/2 protocol, so grab Hyper

pip install hyper

Generate an auth key

Now, head over to the Apple member center and generate an APNS auth key. In the Certificates, Identifiers & Profiles section of the Member Center, under Certificates there is a new section APNs Auth Key. There click the add button to create a new key.

Auth Key Step 1

Under Production select Apple Push Notification Authentication Key (Sandbox & Production) and click continue and a key will be created for you.

2016-9-24 APNS auth key

Download the .p8 file and note the key ID and the .p8 filename, as we'll need those in a moment.

APNS_KEY_ID = 'ABC123DEFG'
APNS_AUTH_KEY = '/PATH_TO/APNSAuthKey_ ABC123DEFG.p8'
Auth key step 3

While you're in the member center, grab your Team ID as well in the membership area.

TEAM_ID = 'DEF123GHIJ'

Generate an auth token

APNS wants the JSON web token in the format:

Header
{
    "alg": "ES256",
    "kid": "ABC123DEFG"
}
Claim
{
    "iss": "DEF123GHIJ",
    "iat": 1437179036
}
SECRET
  • alg (Algorithm): The required encoding algorithm.
  • kid (Key ID): The ID of the key you generated
  • iss (Issuer): Your developer team ID
  • iat (Issued At): Number of seconds from Epoch in UTC when the token was generated

Using the information we collected above, we now have everything we need to generate our token.

import jwt
import time

from hyper import HTTPConnection

ALGORITHM = 'ES256'

APNS_KEY_ID = 'ABC123DEFG'
APNS_AUTH_KEY = '/PATH_TO/APNSAuthKey_ ABC123DEFG.p8'
TEAM_ID = 'DEF123GHIJ'

f = open(APNS_AUTH_KEY)
secret = f.read()

token = jwt.encode(
    {
        'iss': TEAM_ID,
        'iat': time.time()
    },
    secret,
    algorithm= ALGORITHM,
    headers={
        'alg': ALGORITHM,
        'kid': APNS_KEY_ID,
    }
)

Sending a notification

Now that we have our token, we can use it to send a push notification. Apple specifies the following format for a push request:

HEADERS
  - END_STREAM
  + END_HEADERS
  :method = POST
  :scheme = https
  :path = /3/device/DEVICE_REGISTRATION_ID
  host = api.development.push.apple.com
  authorization = bearer AUTH_TOKEN
  apns-id = OPTIONAL_UUID
  apns-expiration = 0
  apns-priority = 10
  apns-topic = TOPIC
DATA
  + END_STREAM
    { "aps" : { "alert" : "Hello" } }

A few things to note:

  • path: Be sure to use the hexadecimal bytes of the device token for the device you are sending a notification to.
  • apns-topic: This is required. If you're not already using it for something specific, use your bundle ID.
  • host: For production, use api.push.apple.com

And now we have everything we need to send a push request:

import json

BUNDLE_ID = 'com.example.CheerfulBirds'

REGISTRATION_ID = '00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0'

path = '/3/device/{0}'.format(REGISTRATION_ID)

request_headers = {
    'apns-expiration': '0',
    'apns-priority': '10',
    'apns-topic': BUNDLE_ID,
    'authorization': 'bearer {0}'.format(token.decode('ascii'))
}
payload_data = { 
    'aps': { 'alert' : 'Oh hai!' } 
}
payload = json.dumps(payload_data).encode('utf-8')

conn = HTTPConnection('api.development.push.apple.com:443')

conn.request(
    'POST', 
    path, 
    payload, 
    headers=request_headers
)

resp = conn.get_response()
print(resp.status)
print(resp.read())

If the server returns a 200 status with an empty body, success!! Otherwise, if the error message isn't self explanatory, check the table here

That's it!

Finally, the code in it's entirety

import json
import jwt
import time

from hyper import HTTPConnection

ALGORITHM = 'ES256'

APNS_KEY_ID = 'ABC123DEFG'
APNS_AUTH_KEY = '/PATH_TO/APNSAuthKey_ ABC123DEFG.p8'
TEAM_ID = 'DEF123GHIJ'
BUNDLE_ID = 'com.example.CheerfulBirds'

REGISTRATION_ID = '00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0'

f = open(APNS_AUTH_KEY)
secret = f.read()

token = jwt.encode(
    {
        'iss': TEAM_ID,
        'iat': time.time()
    },
    secret,
    algorithm= ALGORITHM,
    headers={
        'alg': ALGORITHM,
        'kid': APNS_KEY_ID,
    }
)

path = '/3/device/{0}'.format(REGISTRATION_ID)

request_headers = {
    'apns-expiration': '0',
    'apns-priority': '10',
    'apns-topic': BUNDLE_ID,
    'authorization': 'bearer {0}'.format(token.decode('ascii'))
}

# Open a connection the APNS server
conn = HTTPConnection('api.development.push.apple.com:443')

payload_data = { 
    'aps': { 'alert' : 'All your base are belong to us.' } 
}
payload = json.dumps(payload_data).encode('utf-8')

# Send our request
conn.request(
    'POST', 
    path, 
    payload, 
    headers=request_headers
)
resp = conn.get_response()
print(resp.status)
print(resp.read())

# If we are sending multiple requests, use the same connection

payload_data = { 
    'aps': { 'alert' : 'You have no chance to survive. Make your time.' } 
}
payload = json.dumps(payload_data).encode('utf-8')

conn.request(
    'POST', 
    path, 
    payload, 
    headers=request_headers
)
resp = conn.get_response()
print(resp.status)
print(resp.read())