How to access Apple’s App Connect API from C#, Python, and Go. – Part 2

In my previous post, I wrote about a need to query my company’s membership list from our Apple app store development account. In that post, I used C# to query Apple’s API.  In this installment, we’ll cover how to accomplish the same task with Python.  The final post will cover a Go version.

As with the C# version, we’ll need to create a signed Javascript Web Token (JWT) and then make some API calls.  As with the C# version, I have the code in a Github repository. You can clone it from here. The code is in a script named IsUserInApple.py. You can name your script anything that the OS and Python allows, I’ll be referring to my script as IsUserInApple.py.

I wrote the code using Python 3, version 3.9.2. Unless you are running Windows, the odds are that you have Python 3 installed. If not, you can get it from the good people at python.org. This code should work on any platform that supports Python 3. It should also work (potentially with some changes) with Python 2, but I haven’t tested that. 

This script will use a couple of libraries that you will need to install. To make the HTTPS web requests, we’ll be using the Requests library. It does exactly what it says on the tin, “Requests is an elegant and simple HTTP library for Python, built for human beings”. It makes the code for calling Apple’ API simple and easy to follow.

The other library is Authlib, a library for working with OAuth and OpenID Connect. It has everything we need to create and sign our JWT.  While we need to install all of Authlib, we’ll only being using the jwt module.  To install the libraries, you just need to run the pip command like this:

pip install requests authlib

If you are not familar with pip. it’s the package manager for Python and will be installed when you install Python. If you already have Python installed, then you already have pip installed.

In the folder that you will have the Python script, you will need a configuration file. It performs the same function as the IsUserInApple.json file did in the C# code. It will contain the path to your private key file, the key id, and the issuer id for your account. Please refer back to Part 1 for how to define these values. Instead of using JSON, I used a simple key/value file and it’s named IsUserInApple.config. It should use the following format:

[settings]
private_key = c:/scripts/AuthKey.p8
KEY_ID = ABCDEF1234
ISSUER_ID = d88b7c23-4c26-48fb-9d62-5649f27a25a2

Now we can go through the code. I’m going to jump around a bit, you may want to have the script open in another window. After some comments, we start with the following lines:

import requests, time, json, sys, tempfile, os, configparser
from authlib.jose import jwt

The first line imports the libraries that we’ll need. The requests library was the 3rd party library that we installed via pip, the rest are libraries included with Python. The next line imports the jwt model from the authlib.jose library. The “jose” part of the library name is an acronym for Javascript Object Signing and Encryption.

The next part of the code are the methods we’ll define and use to call the API. We’ll dive into them in a bit. For now, we’ll jump down to the bottom of the script.  We start with

if len(sys.argv) > 1:
    config = configparser.ConfigParser()
    try:
        configPath = os.path.dirname(os.path.abspath(__file__)) + "/IsUserInApple.config"
        config.read(configPath)
    except Exception as e :
        print(str(e))

Line 1 is saying “if we have at least one command line parameter after the name of the script…”. The next line creates an instance of the ConfigParser class, which will allow us to easily read contents from the IsUserInApple.config configuation file. Starting at line 3, we’ll use a try/except block to read the config file. We want to read the config file from the same folder as the script. To get that folder name, we call the abspath method on the __file__ variable. The __file__variable is a “dunder” variable in Python and represents the name of the currently running module. A list of dunder variables can be found in the Python Docs. The dirname method will return the folder part of the file name and then we concatenate that with the name of the config file. And at line 5, we read the config file.

Next, we read the settings into variables with the following code. It’s pretty much self explanatory.

try:
    private_key = config['settings']['private_key']    
    KEY_ID = config['settings']['KEY_ID']    
    ISSUER_ID = config['settings']['ISSUER_ID']    
except Exception as e :
    print(str(e))

Next, we have the following code:

if not os.path.isfile(private_key):
    sys.exit("Error missing private key file for JWT")

UserEmail = sys.argv[1].lower()

print('Looking for a match on ' + UserEmail)

token = getToken(KEY_ID, ISSUER_ID, private_key)

First we check to see if the value provided for private_key is actually a file. Then we set UserEmail to the command line parameter (while converting it to lowercase) and echo that back to the shell. The last line is where we call our getToken method in the script to generate the signed JWT. Now, we’ll jump to that getToken method.

def getToken(KEY_ID, ISSUER_ID, PATH_TO_KEY):
    EXPIRATION_TIME = int(round(time.time() + (20.0 * 60.0))) # 20 minutes timestamp

    with open(PATH_TO_KEY, 'r') as f:
        PRIVATE_KEY = f.read()

    header = {
        "alg": "ES256",
        "kid": KEY_ID,
        "typ": "JWT"
    }

    payload = {
        "iss": ISSUER_ID,
        "exp": EXPIRATION_TIME,
        "aud": "appstoreconnect-v1"
    }

    # Create and return the JWT
    return jwt.encode(header, payload, PRIVATE_KEY)

Line 1 defines the name of the method and the parameter names. No big whoop. Line 2 generates the expiration time, 20 minutes into the future. At line 4, we read in the private key file. The jwt module groks the PEM format, we don’t have to clean it up like we did in the C# code. Then line 7 creates the header and line 13 creates the payload. This is nearly identical to the code from the C# version. At line 20, we create and sign the JWT and return to the code that called it. It follows the same logic as the code in the GetToken method from AppleJWT.cs. Jumping back to where we called getTokem, we have the following line:

members = getAllUsers(token)

Now we’ll dive into the getAllUsers method.

def getAllUsers(token):
    JWT = 'Bearer ' + token.decode()
    URL = 'https://api.appstoreconnect.apple.com/v1/users?limit=100'
    HEAD = {'Authorization': JWT}

    teamMembers = []

    nextURL = URL
    keepGoing = True

    while keepGoing:
        r = requests.get(nextURL, params={}, headers=HEAD)

        y = r.json()

        if 'errors' in y:
            errorCode = y['errors'][0]
            print('Apple returned an HTTP ' + errorCode['status'] + ' code')
            print(errorCode['detail'])
            sys.exit('whoops')

        for i in y['data']:
            teamMembers.append({'username':i['attributes']['username'].lower(), 'roles': ','.join(i['attributes']['roles'])})

        if 'next' in y['links']:
            nextURL = y['links']['next']
        else:
             keepGoing = False

    return teamMembers

Line 1 defines the method name and parameters. Lines 2-4 sets the bearer (token) authorization and the inital URL to call. The next few lines just initialize a few variables and the fun begins at line 11 where we’ll loop until we are done. At line 12, we use the requests library to make a HTTPS get call. Line 14 assigns the JSON results to our Y variable. The next block of code checks the JSON document for “errors”. If Apple’s API falls down, it will fall down this way.

If there are no errors, we just walk through the “data” member of JSON document and add the username and roles for each user to the teamMembers array. When we append the username, we convert it to lowercase. That was we don’t have to worry about case when we match on the email address. Then we check the see if the “links” member of the document has a field named “next”. If it does, we use that as the new URL and keep looping. When we don’t have a “next” url, we return the teamMembers array to caller. That takes us back to the bottom of our script again.

    for i in members:
        if i['username'] == UserEmail:
            HasMatch = True
            print('Match on ' + i['username'] + ', Roles: ' + i['roles'])

    if HasMatch == False:
        print('No match')

else:
    sys.exit("Error: Please specify an email address")

Now we just iterate through the members array and see if the user name matches. We report back on whether we foiund it or not.  The last lines are the error message that would be reported if you ran the script without a email address to match on.

That’s basically all there is to this script. This version doesn’t deserialize the JSON data into objects, it just parses it as is. It’s a trade off. Doing it this way uses less code, but you lose some of the discoverability of having the data in object form.

To run it, you would just do something like:

python IsUserInApple.py some.email@company.com

You’ll either the following if that use is a member of the account:

Looking for a match on some.email@company.com
Match on some.email@company.com, Roles: ADMIN,ACCOUNT_HOLDER

Or the following if that email doesn’t provide a match

Looking for a match on some.email@company.com
No match

Coming up next will be the Go version of this script.


About the image:
This image was derived from some open-source images. The image of the wall comes from Patrick Tomasso via Unsplash. The Apple lock logo comes from Apple, which retains all rights to its artwork. The people icon was created by Monika from the Noun Project.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.