GoでApple Store Server APIのリクエスト

## 概要

本記事では、Apple Store Server API のリクエストに必要な JWT について説明し、Go言語で JWT を生成・使用する方法を紹介する。  
また、実際に生成したJWTを利用して、Apple Server Notifications API のテストを要求する方法を紹介する。

## JWT とは

JWT は、[JSON Web Token](https://tools.ietf.org/html/rfc7519) の略で、属性情報を JSON 形式で記述したトークンのことである。  
JWT には関連する仕様として、[JWS](https://tools.ietf.org/html/rfc7515) と [JWE](https://tools.ietf.org/html/rfc7516) がある。  
JWS は、JSON 形式で署名されたデータを記述する仕様であり、JWE は、JSON 形式で暗号化されたデータを記述する仕様である。  
Apple Store Server API では、JWS を利用するため、今回は JWS を用いた JWT の生成を行う。

なお、Apple Store Server API のリクエスト用トークンの生成方法は[こちら](https://developer.apple.com/documentation/appstoreserverapi/generating_tokens_for_api_requests)に記載されている。  
以降は、このドキュメントを参考に説明する。

## JWT の構成

JWT は、3つの部分に分かれている。

- Header
- Payload
- Signature

各部分は、Base64URL でエンコードされ、 `.` で連結した文字列で表現される。

```text
<BASE64URL Header>.<BASE64URL Payload>.<BASE64URL Signature>
```

### Header

Headerには、署名に使用するアルゴリズムなどの情報を含む。  
Apple Store Server API では、以下のキーを利用する。

```json
{
"alg": "ES256",
"kid": "2X9R4HXF34",
"typ": "JWT"
}
```

- alg: 署名に使用するアルゴリズム。ES256 を利用する。
- kid: 秘密鍵のID。Apple Store Connect のページから取得する。
- typ: JWT を指定する。

### Payload

Payload には、Apple Store Server API で利用する情報を含む。

```json
{
  "iss": "57246542-96fe-1a63e053-0824d011072a",
  "iat": 1623085200,
  "exp": 1623086400,
  "aud": "appstoreconnect-v1",
  "bid": "com.example.testbundleid2021"
}

```

- iss: 発行者のID。Apple Store Connect のページから取得する。
- iat: 署名の発行日時。UNIX時間で指定する。
- exp: 署名の有効期限。UNIX時間で指定する。
- aud: appstoreconnect-v1 を指定する。
- bid: アプリのバンドルID。Apple Store Connect のページから取得する。

### Signature

Signatureには、Header の `kid` で指定した秘密鍵を使用して、`alg` の署名アルゴリズムで署名したデータを含む。  
このとき、Base64Urlエンコードした Header と Payload を `.` で連結した文字列を署名対象とする。

## JWT の生成

JWT の生成には、[jwg-go](https://github.com/golang-jwt/jwt) を利用する。

```go
func CreateJWT() string {
    // Payload
    now := time.Now()
    payload := jwt.MapClaims{
        "iss": "57246542-96fe-1a63e053-0824d011072a",
        "iat": json.Number(strconv.FormatInt(now.Unix(), 10)),
        "exp": json.Number(strconv.FormatInt(now.Add(20*time.Minute).Unix(), 10)),
        "aud": "appstoreconnect-v1",
        "bid": "com.example.testbundleid2021",
    }

    // JWT 生成
    token := jwt.NewWithClaims(jwt.SigningMethodES256, payload)
    token.Header["kid"] = "2X9R4HXF34"

    // 秘密鍵の読み込み
    keyFile, err := ioutil.ReadFile("YOUR_PRIVATE_KEY.p8")
    if err != nil {
        panic(err)
    }

    key, err := jwt.ParseECPrivateKeyFromPEM(keyFile)
    if err != nil {
        panic(err)
    }

    // JWT の署名
    signedToken, err := token.SignedString(key)
    if err != nil {
        panic(err)
    }

    fmt.Println(signedToken)

    return signedToken
}
```

## JWT の利用方法

JWT を利用するには、リクエストヘッダに `Authorization` を指定し、 `Bearer` とトークンを指定する。

```text
Authorization: Bearer <JWT>
```

これを使ってGoでサンドボックス環境のサーバ通知テストをリクエストする関数を実装すると以下のようになる。

[https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification]

```go
func RequestTestNotification(token string) {
    httpClient := http.DefaultClient

    req, err := http.NewRequestWithContext(context.Background(), "POST", "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/notifications/test", nil)
    if err != nil {
        panic(err)
    }

    req.Header.Set("Authorization", "Bearer "+token)

    resp, err := httpClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(body))
}
```

## 最後に

今回は、サーバ通知APIのテストをリクエストを利用するために、Apple Store Server API を利用する必要があったのですが、あまりGo言語を使った記事を見かけなかったので、書いてみました。  
一応、調べて動かしながら書いたのですが、間違っているところがあればご指摘いただけると幸いです。