Apple has an option to access their App Store using an API as described in their documentation. Let's see how we can use this API using Golang. We're assuming you already have an Apple developer account setup.

The API is a REST API using a JSON Web Token (JWT) for authentication.

Before we can do anything, we need to create the keys needed to be able to connect. You can do this under "Users and Access" in the App Store Connect website:

  1. Browse to the App Store Connect website
  2. Login to your developer account
  3. Select "Users and Access" and go the tab "Keys"
  4. In the left column, ensure you have "App Store Connect API" selected
  5. Create a new key by clicking on the plus sign
  6. Give the key a name and select the access level (I selected "Admin")

One you did this, you'll end up with 3 pieces of information:

  • Issuer ID: Identifies the issuer who created the authentication token
  • Key ID: the unique ID of the key which was issued
  • Key (a .p8 file): the actual API key you created

You then download the .p8 file and store it in a safe location. Remember you can download this file only once. If you loose it, you'll need to generate a new API key.

We'll now start a new empty Go project to have a look at how to connect:

$ mkdir appstoreconnectapi
$ cd appstoreconnectapi
$ go mod init github.com/pieterclaerhout/appstoreconnectapi

To make it easier, I'll put a copy of the .p8 in that same folder. We also create a main.go file which is the main entry point for our sample app. In there, let's start with defining the connection details:

package main

const issuerID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
const keyID = "XXXXXXXXXX"
const keyFile = "AuthKey_XXXXXXXXXX.p8"

func main() {
}

We'll then add the parsing of the .p8 file as we need it to create the connection:

package main

import (
    "crypto/ecdsa"
    "crypto/x509"
    "encoding/pem"
    "errors"
    "io/ioutil"
)

const issuerID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
const keyID = "XXXXXXXXXX"
const keyFile = "AuthKey_XXXXXXXXXX.p8"

func main() {

    privateKey, err := privateKeyFromFile()
    if err != nil {
        log.Fatal(err)
    }

}

func privateKeyFromFile() (*ecdsa.PrivateKey, error) {

    bytes, err := ioutil.ReadFile(keyFile)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(bytes)
    if block == nil {
        return nil, errors.New("AuthKey must be a valid .p8 PEM file")
    }
    key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    switch pk := key.(type) {
    case *ecdsa.PrivateKey:
        return pk, nil
    default:
        return nil, errors.New("AuthKey must be of type ecdsa.PrivateKey")
    }

}

The next step is adding the code used to generate the authentication token. We'll use the jwt-go library to generate the authentication key.

package main

import (
    "crypto/ecdsa"
    "crypto/x509"
    "encoding/pem"
    "errors"
    "io/ioutil"
    "log"
    "time"

    "github.com/dgrijalva/jwt-go"
)

const issuerID = "69a6de7f-828a-47e3-e053-5b8c7c11a4d1"
const keyID = "KB22YXSRT2"
const keyFile = "AuthKey_KB22YXSRT2.p8"

func main() {

    privateKey, err := privateKeyFromFile()
    if err != nil {
        log.Fatal(err)
    }

    authToken, err := generateAuthToken(privateKey)
    if err != nil {
        log.Fatal(err)
    }

}

func privateKeyFromFile() (*ecdsa.PrivateKey, error) {

    bytes, err := ioutil.ReadFile(keyFile)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(bytes)
    if block == nil {
        return nil, errors.New("AuthKey must be a valid .p8 PEM file")
    }
    key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    switch pk := key.(type) {
    case *ecdsa.PrivateKey:
        return pk, nil
    default:
        return nil, errors.New("AuthKey must be of type ecdsa.PrivateKey")
    }

}

func generateAuthToken(privateKey *ecdsa.PrivateKey) (string, error) {

    expirationTimestamp := time.Now().Add(15 * time.Minute)

    token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
        "iss": issuerID,
        "exp": expirationTimestamp.Unix(),
        "aud": "appstoreconnect-v1",
    })

    token.Header["kid"] = keyID

    tokenString, err := token.SignedString(privateKey)
    if err != nil {
        return "", err
    }

    return tokenString, nil

}

Now we have all the bits and pieces we need to perform a request. In this example, we request the latest 10 builds from our account using the v1/builds endpoint.

package main

import (
    "crypto/ecdsa"
    "crypto/x509"
    "encoding/pem"
    "errors"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "time"

    "github.com/dgrijalva/jwt-go"
)

const issuerID = "69a6de7f-828a-47e3-e053-5b8c7c11a4d1"
const keyID = "KB22YXSRT2"
const keyFile = "AuthKey_KB22YXSRT2.p8"

func main() {

    privateKey, err := privateKeyFromFile()
    if err != nil {
        log.Fatal(err)
    }

    authToken, err := generateAuthToken(privateKey)
    if err != nil {
        log.Fatal(err)
    }

    client := &http.Client{}

    qs := url.Values{}
    qs.Set("sort", "-uploadedDate")
    qs.Set("limit", "10")

    req, err := http.NewRequest(
        http.MethodGet,
        "https://api.appstoreconnect.apple.com/v1/builds?"+qs.Encode(),
        nil,
    )
    if err != nil {
        log.Fatal(err)
    }

    req.Header.Set("Authorization", "Bearer "+authToken)
    req.Header.Set("User-Agent", "App Store Connect Client")

    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    log.Println(string(bytes))

}

func privateKeyFromFile() (*ecdsa.PrivateKey, error) {

    bytes, err := ioutil.ReadFile(keyFile)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(bytes)
    if block == nil {
        return nil, errors.New("AuthKey must be a valid .p8 PEM file")
    }
    key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    switch pk := key.(type) {
    case *ecdsa.PrivateKey:
        return pk, nil
    default:
        return nil, errors.New("AuthKey must be of type ecdsa.PrivateKey")
    }

}

func generateAuthToken(privateKey *ecdsa.PrivateKey) (string, error) {

    expirationTimestamp := time.Now().Add(15 * time.Minute)

    token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
        "iss": issuerID,
        "exp": expirationTimestamp.Unix(),
        "aud": "appstoreconnect-v1",
    })

    token.Header["kid"] = keyID

    tokenString, err := token.SignedString(privateKey)
    if err != nil {
        return "", err
    }

    return tokenString, nil

}

Next time, we can look at how to do error handling and how to properly interpret the errors.

Related Posts

  • Parsing App Store Connect API errors with Go
  • Detecting Apple Silicon via Go
  • Golang vs Apple Silicon
  • Using the Docker client from Go part 2
  • First beta of Go 1.16 is now available