😼

シェルスクリプトで GitHub App のインストールアクセストークンを取得する

2021/06/27に公開
1

概要

GitHub App を使うと個人アカウントに紐付いたトークンに依存せず GitHub の API を実行することができて便利です。
ただ、GitHub App の権限で API を実行するためには、

  1. GitHub App の管理画面で発行した秘密鍵を使って JWT (JSON Web Token) に署名する
  2. 署名した JWT でインストールアクセストークン(installation access token)を取得する
  3. インストールアクセストークンを使って API を実行する

という手順を踏む必要があり、やや面倒です。
GitHub の公式ドキュメントでは Ruby のサンプルコードが提示されていますが、今回はシェルスクリプトでこのフローを実装してみました。

Bash と date, base64, openssl, curl, jq といった基本的なコマンドだけを使って実装しています。
jq は頑張れば grepsed でも代用可能だと思うので、この方法であれば大抵の Linux 系環境で GitHub App の権限を使った API 実行ができるのではないでしょうか。

手順

準備

まず GitHub App を作成し、秘密鍵を発行しておいてください。
今回のサンプルコードをそのまま動かすためには、その GitHub App を自身が所有する適当なリポジトリにインストールしておく必要があります。

JWT のヘッダーを作る

GitHub の公式ドキュメントに書いてあるとおり RS256 アルゴリズムを使用するので、以下のような JSON を base64 エンコードしたものがヘッダーになります。

{
  "alg": "RS256",
  "typ": "JWT"
}

この JSON を echo して base64 コマンドでエンコードすればいいんですが、base64 コマンドはデフォルトで76文字毎に改行を入れるので、それを止めるために -w 0 というオプションを付けます。
なお、macOS の base64 コマンドはデフォルトで改行を入れないので、このオプションは不要です。

header=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 -w 0)

JWT のペイロードを作る

同じく GitHub の公式ドキュメントにあるとおり、以下のような JSON を base64 エンコードしたものがペイロードになります。

{
  "iat": <発行時刻 (UNIX時間)>, // 時計がずれている可能性を考慮して現在時刻の1分前に設定
  "exp": <失効時刻 (UNIX時間)>, // ドキュメントの通り、現在時刻の10分後に設定
  "iss": <GitHub App の ID>
}

date コマンドで現在時刻の UNIX 時間を取得し、"iat""exp" を計算します。

now=$(date "+%s")
iat=$((${now} - 60))
exp=$((${now} + (10 * 60)))
payload=$(echo -n "{\"iat\":${iat},\"exp\":${exp},\"iss\":${github_app_id}}" | base64 -w 0)

ヘッダーとペイロードを連結し、秘密鍵で署名する

ヘッダーとペイロードを "." で連結し、openssl コマンドで GitHub App の秘密鍵を使って署名します。
署名したものを base64 エンコードして、更にヘッダー、ペイロードと "." で連結すれば JWT の完成です。

unsigned_token="${header}.${payload}"
signed_token=$(echo -n "${unsigned_token}" | openssl dgst -binary -sha256 -sign "${github_app_key_path}" | base64)

jwt="${unsigned_token}.${signed_token}"

installation ID を特定する

以上の手順で作った JWT を使うことで、GitHub App の installation 一覧を取得する API とインストールアクセストークンを取得する API を実行することができます。
installation とは GitHub App が特定の user や organization にインストールされていることを表す概念です。 GitHub App はこの installation 毎にアクセストークンを発行する(=インストールアクセストークン)ため、まずは最終的に API を実行したい対象の user や organization の installation ID を特定する必要があります。

installation 一覧を取得する API は以下のような JSON を返してくるので、.account.login.account.type を使って目的の user や organization の installation を特定します。

[
  {
    "id": 12345,
    "account": {
      "login": <user名>,
      "type": "User",
      ...
    },
    ...
  },
  {
    "id": 67890,
    "account": {
      "login": <organization名>,
      "type": "Organization",
      ...
    },
    ...
  },
  ...
]

ということで、以下のように curl コマンドで取得した API の実行結果から jq で目的のユーザーの installation ID を抽出します。
お察しの通り、organization の場合は同じ要領で .account.type"Organization" のものを抽出すれば良いです。

installation_id=$(
  curl -s -X GET \
    -H "Authorization: Bearer ${jwt}" \
    -H "Accept: application/vnd.github.v3+json" \
    "https://api.github.com/app/installations" \
  | jq -r ".[] | select(.account.login == \"${user_name}\" and .account.type == \"User\") | .id"
)

インストールアクセストークンを取得する

これでようやくインストールアクセストークンを取得する準備が整いました。
認証情報として JWT を使い、パスに上で特定した installation ID を入れて API を実行すればインストールアクセストークンを取得することができます。

installation_token=$(
  curl -s -X POST \
    -H "Authorization: Bearer ${jwt}" \
    -H "Accept: application/vnd.github.v3+json" \
    "https://api.github.com/app/installations/${installation_id}/access_tokens" \
  | jq -r ".token"
)

目的の API を実行する

いよいよ本当に実行したかった API を叩くことができます。
今回は試しにこのトークンで read できるリポジトリ一覧を取得する API を実行してみましょう。

ヘッダーに Authorization: token <インストールアクセストークン> を指定すれば OK です。

curl -s -X GET \
  -H "Authorization: token ${installation_token}" \
  -H "Accept: application/vnd.github.v3+json" \
  "https://api.github.com/installation/repositories"

まとめ

ということで、以下のとおり GitHub App のインストールアクセストークンを取得するシェルスクリプトが完成しました。

#!/usr/bin/env bash

set -euox pipefail

user_name="<GitHub のユーザー名>"
github_app_id="<GitHub App の ID>"
github_app_key_path="private-key.pem" # GitHub App の秘密鍵が置かれているパス

header=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 -w 0)

now=$(date "+%s")
iat=$((${now} - 60))
exp=$((${now} + (10 * 60)))
payload=$(echo -n "{\"iat\":${iat},\"exp\":${exp},\"iss\":${github_app_id}}" | base64 -w 0)

unsigned_token="${header}.${payload}"
signed_token=$(echo -n "${unsigned_token}" | openssl dgst -binary -sha256 -sign "${github_app_key_path}" | base64 -w 0)

jwt="${unsigned_token}.${signed_token}"

installation_id=$(
  curl -s -X GET \
    -H "Authorization: Bearer ${jwt}" \
    -H "Accept: application/vnd.github.v3+json" \
    "https://api.github.com/app/installations" \
  | jq -r ".[] | select(.account.login == \"${user_name}\" and .account.type == \"User\") | .id"
)

installation_token=$(
  curl -s -X POST \
    -H "Authorization: Bearer ${jwt}" \
    -H "Accept: application/vnd.github.v3+json" \
    "https://api.github.com/app/installations/${installation_id}/access_tokens" \
  | jq -r ".token"
)

curl -s -X GET \
  -H "Authorization: token ${installation_token}" \
  -H "Accept: application/vnd.github.v3+json" \
  "https://api.github.com/installation/repositories"

ご覧の通り、JWT を自前で作るとなかなか面倒くさいスクリプトになるので、JWT 系のライブラリが使えるスクリプト言語が入っているならおとなしくそちらを使ったほうが良いでしょう。
もしそういうものが入っていない環境で GitHub App の権限を使って API 実行したくなったら参考にしてみてください。

Discussion