We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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:
- Browse to the App Store Connect website
- Login to your developer account
- Select "Users and Access" and go the tab "Keys"
- In the left column, ensure you have "App Store Connect API" selected
- Create a new key by clicking on the plus sign
- 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.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.