TORIPIYO DIARY

recent events, IoT, programming, security topics

GitHub Apps からトークンを生成して GitHub API を利用する

GitHub API を利用するプログラムを書くときは、パーソナルアクセストークンを利用するのがお手軽なやり方です。

しかし、GitHub Apps を利用すると、

  • レポジトリ単位で権限設定を柔軟にできる
  • トークンの有効期限が短い
  • トークンが個人に紐付かないので異動や退職した時の影響が小さい

という優れた点を享受できます。GitHub Apps はトークン発行の手順がパーソナルアクセストークンに比べると複雑なので、本記事で紹介したいと思います。

手順は、以下です。 (前提として、GitHub Apps を作成して、Organization に GitHub Appsをインストール設定済み、GitHub Apps の設定から秘密鍵を生成済みとします。)

トークン取得手順

JWTデータの生成

トークンを取得する前に、まず GitHub Apps の認証をします。認証には、GitHub Apps の設定画面から生成した秘密鍵で署名した、JWTデータを利用します。

以下は、GitHub のドキュメントに掲載されているJWTデータを生成するRubyスクリプトの例です(jwt の gem をインストールしておく必要あり)。

  • 秘密鍵のファイルと、GitHub Apps の ID は、GitHub Apps の設定から取得できます。
  • このJWTの有効期限は生成から10分に設定されています。

jwt.rb

require 'openssl'
require 'jwt'  # https://rubygems.org/gems/jwt

# Private key contents
private_pem = File.read(ARGV[0])
private_key = OpenSSL::PKey::RSA.new(private_pem)

# Generate the JWT
payload = {
  # issued at time, 60 seconds in the past to allow for clock drift
  iat: Time.now.to_i - 60,
  # JWT expiration time (10 minute maximum)
  exp: Time.now.to_i + (10 * 60),
  # GitHub App's identifier
  iss: "ARGV[1]"
}

jwt = JWT.encode(payload, private_key, "RS256")
puts jwt

Installationの選定

上記 Ruby スクリプトを実行することで得られたJWTデータを利用して、Installation のリストを取得します。Installation とは、GitHub Apps をインストールしている主体のことです。例えば、GitHub Apps を公開して、様々な個人ユーザーのアカウントや企業の Organization にインストールされたとすると、このインストールをした一つ一つの主体が Installation となります。トークンは Installation の単位で発行されるので、トークンを生成するときにはどの Installation を対象にトークンを発行するのか決めるために、Installation ID を指定する必要があります。

以下は、Installation のリストを取得するための curl リクエストです。${JWT} のところに、前節で生成したJWTの値を入れます。

$ curl -i -X GET \
-H "Authorization: Bearer ${JWT}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/app/installations

リクエストが成功すると、以下の応答例のように Installation の一覧を取得できます。応答データから、トークンを発行したい Installation の id を控えます。もし、 GitHub Apps をプライベートに設定していると、GitHub Apps のオーナーのみが GitHub Apps をインストールできる設定となっているので、自作のGitHub Apps をインストールした状態であれば、Installation の数はおそらく1つになります。

https://api.github.com/app/installations の応答例

[
  {
    "id": 1,
    "account": {
      "login": "octocat",
      "id": 1,
      "node_id": "MDQ6VXNlcjE=",
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "access_tokens_url": "https://api.github.com/installations/1/access_tokens",
    "repositories_url": "https://api.github.com/installation/repositories",
    "html_url": "https://github.com/organizations/github/settings/installations/1",
    "app_id": 1,
    "target_id": 1,
    "target_type": "Organization",
    "permissions": {
      "checks": "write",
      "metadata": "read",
      "contents": "read"
    },
    "events": [
      "push",
      "pull_request"
    ],
    "single_file_name": "config.yaml",
    "has_multiple_single_files": true,
    "single_file_paths": [
      "config.yml",
      ".github/issue_TEMPLATE.md"
    ],
    "repository_selection": "selected",
    "created_at": "2017-07-08T16:18:44-04:00",
    "updated_at": "2017-07-08T16:18:44-04:00",
    "app_slug": "github-actions",
    "suspended_at": null,
    "suspended_by": null
  }
]

トークンの生成

JWTを生成して、トークン発行対象のInstallation ID も決まったら、以下のように、トークンの生成リクエストを GitHub API に送ります。

${YOUR_JWT} には、生成したJWTの値、${installation_id}には、前段で控えた Installation の id を設定します。

curl -i -X POST \
-H "Authorization: Bearer ${YOUR_JWT}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/app/installations/${installation_id}/access_tokens

GitHub API へのリクエストに成功すると、応答のjsonデータからトークン値を取得できます。

{
  "token": "ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "expires_at": "2022-10-02T13:39:20Z",
  "permissions": {
    "metadata": "read"
  },
  "repository_selection": "selected"
}

GitHub API と通信

取得したトークン値を利用して、GitHub Apps をインストールした時に許可したパーミッションの範囲内で GitHub リソースの読み込みや変更を行うことができます。

例えば、GitHub Apps で Contents の Read-only パーミッションをインストール時に許可されれば、

以下のコマンドでトークンを利用して git clone を実行できます。

git clone https://x-access-token:${TOKEN}@github.com/owner/repo.git

また、以下のコマンドを使えば、対象レポジトリのコミットの一覧を取得できます。

curl \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer ${TOKEN}" \
  https://api.github.com/repos/owner/repo/commits

APIのリファレンスを参照しながら実装すれば、様々な GitHub のリソース情報を取得・変更可能です。

手順をコマンドにまとめたもの

ここまでの手順をコマンドにまとめたものが以下です。プライベートの GitHub Apps を作成した前提で、Installation リストの一番最初を Installation ID として設定しています。 トークンを取得すれば、あとは許可された範囲内で GitHub リソースの操作が可能です。

GITHUB_APP_ID=123456
GITHUB_APP_PRIVATE_KEY_PATH="xxxxxx"

# JWT の生成
cat << EOF > jwt.rb
require 'openssl'
require 'jwt'  # https://rubygems.org/gems/jwt

private_pem = File.read(ARGV[0])
private_key = OpenSSL::PKey::RSA.new(private_pem)

payload = {
  # issued at time, 60 seconds in the past to allow for clock drift
  iat: Time.now.to_i - 60,
  # JWT expiration time (10 minute maximum)
  exp: Time.now.to_i + (10 * 60),
  # GitHub App's identifier
  iss: "#{ARGV[1]}"
}

jwt = JWT.encode(payload, private_key, "RS256")
puts jwt
EOF

JWT=$(ruby jwt.rb ${GITHUB_APP_PRIVATE_KEY_PATH} ${GITHUB_APP_ID})

# Installation ID の取得
INSTALLATION_ID=$(curl -s -X GET \
-H "Authorization: Bearer ${JWT}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/app/installations | jq '.[0].id')

# Token の取得
TOKEN=$(curl -s -X POST \
-H "Authorization: Bearer ${JWT}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/app/installations/${INSTALLATION_ID}/access_tokens | jq -r .token)

参考

https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps