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:
1package main
2
3const issuerID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
4const keyID = "XXXXXXXXXX"
5const keyFile = "AuthKey_XXXXXXXXXX.p8"
6
7func main() {
8}
We'll then add the parsing of the .p8
file as we need it to create the connection:
1package main
2
3import (
4 "crypto/ecdsa"
5 "crypto/x509"
6 "encoding/pem"
7 "errors"
8 "io/ioutil"
9)
10
11const issuerID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
12const keyID = "XXXXXXXXXX"
13const keyFile = "AuthKey_XXXXXXXXXX.p8"
14
15func main() {
16
17 privateKey, err := privateKeyFromFile()
18 if err != nil {
19 log.Fatal(err)
20 }
21
22}
23
24func privateKeyFromFile() (*ecdsa.PrivateKey, error) {
25
26 bytes, err := ioutil.ReadFile(keyFile)
27 if err != nil {
28 return nil, err
29 }
30
31 block, _ := pem.Decode(bytes)
32 if block == nil {
33 return nil, errors.New("AuthKey must be a valid .p8 PEM file")
34 }
35 key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
36 if err != nil {
37 return nil, err
38 }
39
40 switch pk := key.(type) {
41 case *ecdsa.PrivateKey:
42 return pk, nil
43 default:
44 return nil, errors.New("AuthKey must be of type ecdsa.PrivateKey")
45 }
46
47}
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.
1package main
2
3import (
4 "crypto/ecdsa"
5 "crypto/x509"
6 "encoding/pem"
7 "errors"
8 "io/ioutil"
9 "log"
10 "time"
11
12 "github.com/dgrijalva/jwt-go"
13)
14
15const issuerID = "69a6de7f-828a-47e3-e053-5b8c7c11a4d1"
16const keyID = "KB22YXSRT2"
17const keyFile = "AuthKey_KB22YXSRT2.p8"
18
19func main() {
20
21 privateKey, err := privateKeyFromFile()
22 if err != nil {
23 log.Fatal(err)
24 }
25
26 authToken, err := generateAuthToken(privateKey)
27 if err != nil {
28 log.Fatal(err)
29 }
30
31}
32
33func privateKeyFromFile() (*ecdsa.PrivateKey, error) {
34
35 bytes, err := ioutil.ReadFile(keyFile)
36 if err != nil {
37 return nil, err
38 }
39
40 block, _ := pem.Decode(bytes)
41 if block == nil {
42 return nil, errors.New("AuthKey must be a valid .p8 PEM file")
43 }
44 key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
45 if err != nil {
46 return nil, err
47 }
48
49 switch pk := key.(type) {
50 case *ecdsa.PrivateKey:
51 return pk, nil
52 default:
53 return nil, errors.New("AuthKey must be of type ecdsa.PrivateKey")
54 }
55
56}
57
58func generateAuthToken(privateKey *ecdsa.PrivateKey) (string, error) {
59
60 expirationTimestamp := time.Now().Add(15 * time.Minute)
61
62 token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
63 "iss": issuerID,
64 "exp": expirationTimestamp.Unix(),
65 "aud": "appstoreconnect-v1",
66 })
67
68 token.Header["kid"] = keyID
69
70 tokenString, err := token.SignedString(privateKey)
71 if err != nil {
72 return "", err
73 }
74
75 return tokenString, nil
76
77}
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.
1package main
2
3import (
4 "crypto/ecdsa"
5 "crypto/x509"
6 "encoding/pem"
7 "errors"
8 "io/ioutil"
9 "log"
10 "net/http"
11 "net/url"
12 "time"
13
14 "github.com/dgrijalva/jwt-go"
15)
16
17const issuerID = "69a6de7f-828a-47e3-e053-5b8c7c11a4d1"
18const keyID = "KB22YXSRT2"
19const keyFile = "AuthKey_KB22YXSRT2.p8"
20
21func main() {
22
23 privateKey, err := privateKeyFromFile()
24 if err != nil {
25 log.Fatal(err)
26 }
27
28 authToken, err := generateAuthToken(privateKey)
29 if err != nil {
30 log.Fatal(err)
31 }
32
33 client := &http.Client{}
34
35 qs := url.Values{}
36 qs.Set("sort", "-uploadedDate")
37 qs.Set("limit", "10")
38
39 req, err := http.NewRequest(
40 http.MethodGet,
41 "https://api.appstoreconnect.apple.com/v1/builds?"+qs.Encode(),
42 nil,
43 )
44 if err != nil {
45 log.Fatal(err)
46 }
47
48 req.Header.Set("Authorization", "Bearer "+authToken)
49 req.Header.Set("User-Agent", "App Store Connect Client")
50
51 resp, err := client.Do(req)
52 if err != nil {
53 log.Fatal(err)
54 }
55 defer resp.Body.Close()
56
57 bytes, err := ioutil.ReadAll(resp.Body)
58 if err != nil {
59 log.Fatal(err)
60 }
61
62 log.Println(string(bytes))
63
64}
65
66func privateKeyFromFile() (*ecdsa.PrivateKey, error) {
67
68 bytes, err := ioutil.ReadFile(keyFile)
69 if err != nil {
70 return nil, err
71 }
72
73 block, _ := pem.Decode(bytes)
74 if block == nil {
75 return nil, errors.New("AuthKey must be a valid .p8 PEM file")
76 }
77 key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
78 if err != nil {
79 return nil, err
80 }
81
82 switch pk := key.(type) {
83 case *ecdsa.PrivateKey:
84 return pk, nil
85 default:
86 return nil, errors.New("AuthKey must be of type ecdsa.PrivateKey")
87 }
88
89}
90
91func generateAuthToken(privateKey *ecdsa.PrivateKey) (string, error) {
92
93 expirationTimestamp := time.Now().Add(15 * time.Minute)
94
95 token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
96 "iss": issuerID,
97 "exp": expirationTimestamp.Unix(),
98 "aud": "appstoreconnect-v1",
99 })
100
101 token.Header["kid"] = keyID
102
103 tokenString, err := token.SignedString(privateKey)
104 if err != nil {
105 return "", err
106 }
107
108 return tokenString, nil
109
110}
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.