Skip to content

GitHub App installation token 認証を追加#25

Merged
zaimy merged 8 commits into
masterfrom
app-auth
Apr 24, 2026
Merged

GitHub App installation token 認証を追加#25
zaimy merged 8 commits into
masterfrom
app-auth

Conversation

@zaimy
Copy link
Copy Markdown
Contributor

@zaimy zaimy commented Apr 24, 2026

Summary

GitHub App の installation token で認証できるようにします。

Changes

  • 環境変数を 3 つ追加しました。3 つすべて設定されていれば App 認証経路で Octocrab クライアントを作ります
    • GITHUB_APP_ID: App ID (整数)
    • GITHUB_APP_PRIVATE_KEY_PATH: App の秘密鍵 (RSA PEM) のファイルパス
    • GITHUB_APP_INSTALLATION_ID: Installation ID (整数)
  • 既存の GITHUB_API_TOKEN と上記 3 変数は排他です。両方セットされている / 片方だけ中途半端に埋まっている状態は起動時に panic して弾きます
  • src/main.rs の認証箇所を Octocrab::builder().app(app_id, key).build()?.installation(installation_id)? に切り替えました。 installation token は octocrab 側が送信直前にキャッシュ検証して必要なら自動で再発行します ( https://github.com/XAMPPRocky/octocrab/blob/v0.49.7/src/lib.rs 内の CachedToken 周辺)
  • Cargo.tomljsonwebtoken = "10" を追加しました。 Octocrab::builder().app(AppId, EncodingKey)jsonwebtoken::EncodingKey を引数に取るためです (octocrab 0.49 側が jsonwebtoken = "10" を使っているのでバージョンを合わせています)
  • README の usage セクションに GitHub App 認証の使い方を追記しました

Review points

  • PEM はファイル経由でのみ読み込みます。環境変数に直接 PEM 本文を入れる経路は意図的にサポートしていません。 /proc/<pid>/environ / クラッシュダンプ / 環境変数を拾うロガー / 子プロセスへの暗黙継承などで秘密鍵が漏れるリスクを減らしたかったためです。 Secret Manager → Cloud Run の連携でもファイルマウント方式 ( --set-secrets "/secrets/github-app-key.pem=projects/.../secrets/.../versions/latest" ) が使えます
  • 排他チェックを match の全パターンで行い、不正な組み合わせで起動した場合は panic! で明示メッセージ出力にしています。 anyhow::bail! にする手もありましたが、 main の戻り値型を変えたくなかったので現状のまま panic にしています
  • installation token の自動リフレッシュは octocrab 側に完全に委譲しています。自前で JWT を生成する必要はありません

Context

octx に GitHub App installation token 認証を追加する 3 本立ての最終 PR です。

3 本立ての最後の PR として、本 PR で Cargo.toml の version を 0.6.20.7.0 に上げています。

zaimy added 2 commits April 24, 2026 14:50
Add three environment variables that, when all present, authenticate
octx as a GitHub App installation:
- GITHUB_APP_ID
- GITHUB_APP_PRIVATE_KEY (RSA PEM body)
- GITHUB_APP_INSTALLATION_ID

GITHUB_API_TOKEN and the App triple are mutually exclusive; exactly
one must be provided. The installation token is obtained by
Octocrab::installation(InstallationId) and refreshed automatically
by octocrab before it expires.
Base automatically changed from bump-octocrab to master April 24, 2026 07:56
zaimy added 3 commits April 24, 2026 16:57
Replace GITHUB_APP_PRIVATE_KEY with GITHUB_APP_PRIVATE_KEY_PATH.
Passing the PEM body through an environment variable exposes it
via /proc/<pid>/environ, core dumps, environment logging, and
child-process inheritance. Reading from disk lets deployments
mount the secret as a file (e.g. Secret Manager volume mount on
Cloud Run) so the raw key never touches the process environment.
@zaimy zaimy marked this pull request as ready for review April 24, 2026 08:13
Copy link
Copy Markdown

@n01e0 n01e0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GHES + App 認証でもページング時に Authorization が付くようにする必要がありそうです。

この PR で追加される App installation 認証は、GHES かつ複数ページになる抽出で 2 ページ目以降が認証なしになりそうです。

1 ページ目は /repos/... のような相対 URI なので installation token の Authorization が付きます。一方、各 fetcher は以降のページ取得で get_page(&page.next) を使っており、page.next は GitHub API の Link ヘッダー由来の絶対 URL です。GHES では https://git.example.com/api/v3/... のような URL になります。

octocrab 0.49 の installation 認証は、動的 Authorization を付ける対象を authority == None または authority == "api.github.com" に限定しています。そのため GHES の絶対 URL には Authorization が付かず、2 ページ目以降が 401/403 になる可能性があります。

page.nextget_page に渡す前に scheme/authority を落として path/query の相対 URI に正規化するなど、GHES + App 認証でもページングが認証付きで続く形にしておきたいです。

octocrab 0.49 only attaches the installation Bearer token when the
outgoing request authority is empty or equals `api.github.com`, so
the absolute URL returned via GitHub's `Link` header (which uses
the GHES authority) would otherwise be sent without Authorization
and fail with 401/403 on page 2 and beyond.

Normalize `page.next` down to path and query before handing it to
`Octocrab::get_page`. `BaseUriLayer` re-applies the base_uri to a
scheme-less and authority-less URI just as it does for the initial
route string, and `AuthHeaderLayer` treats it as a GitHub-destined
request so both Installation and PAT auth continue to work.
@zaimy
Copy link
Copy Markdown
Contributor Author

zaimy commented Apr 24, 2026

@n01e0
ありがとうございます。ご指摘のとおり、octocrab 0.49 には Installation 認証時の Authorization 付与条件が authority == Noneauthority == "api.github.com" にハードコードされている経路があり、GHES の絶対 URL では Authorization が落ちることを実証で確認しました。

内訳としては、auth header の付与が 2 経路に分かれていました。

page.next は Link ヘッダー由来で常に絶対 URL として構築される ( https://github.com/XAMPPRocky/octocrab/blob/v0.49.7/src/page.rs L254-L281 ) ため、 Installation 認証で 2 ページ目以降が認証なしになるというご指摘通りの現象が起きます。

実証として wiremock で 4 シナリオ (Installation / PAT × base_uri と同一 authority / 別 authority) を流し、base_uri と同じ authority (= GHES の通常ケース) でも Installation の 2 ページ目だけ Authorization ヘッダが消えることを確認しました。

[Installation] next=http://127.0.0.1:.../api/v3/next-page?page=2
  GET .../repos/foo/bar/issues        Authorization: Bearer ghs_...
  GET .../next-page?page=2            Authorization: <absent>      ← 消える
[PAT]          next=http://127.0.0.1:.../api/v3/next-page?page=2
  GET .../repos/foo/bar/issues        Authorization: Bearer ghp_...
  GET .../next-page?page=2            Authorization: Bearer ghp_...  ← 付く

修正として、 page.nextget_page に渡す前に scheme と authority を落として path/query だけの URI に正規化するコードを b3b574b で入れました。 to_relative_uri ヘルパを src/lib.rs に追加し、 LoopWriter::write_and_continue のデフォルト実装と、独自にページング処理している events / pulls / reviews / users_detailed / workflows の各 fetcher で適用しています。

authority が落ちると execute()None 分岐で Authorization が付き、 BaseUriLayer が base_uri を再適用するので GHES の /api/v3/ prefix も保持されます。 PAT 認証のときも AuthHeaderLayerNone 許容に引っかかるので挙動は変わりません。

@n01e0
Copy link
Copy Markdown

n01e0 commented Apr 24, 2026

ページング用の page.nextto_relative_uri() 経由になっていて、GHES + App 認証で 2 ページ目以降に Authorization が付かない問題は解消されていそうです。

ただ、users_detailed.rs の詳細取得ではまだ self.octocrab.get(&user.url, ...) に API レスポンス由来の絶対 URL をそのまま渡しています。GHES では user.urlhttps://git.example.com/api/v3/users/... になりますが、octocrab 0.49 の installation 認証は動的 Authorization を付ける対象を authority なし、または api.github.com に限定しています。そのため GHES + App 認証の --users-detailed では、ユーザー詳細取得が認証なしになり 401/403 になる可能性があります。

ここも to_relative_uri(user.url.into()) のように scheme/authority を落としてから get に渡す必要がありそうです。

The users-detailed extractor previously passed the absolute `user.url`
from the list response straight into `Octocrab::get`. On GHES this is
an absolute URL on the GHES authority, which hits the same
installation-auth gap as `page.next` did: octocrab's execute() only
attaches the Bearer token for api.github.com or empty authorities.

Run the URL through `to_relative_uri` first so the request is
dispatched with path+query only, which makes BaseUriLayer re-apply
the base_uri and execute() treat it as a GitHub-destined request.
@zaimy
Copy link
Copy Markdown
Contributor Author

zaimy commented Apr 24, 2026

@n01e0
ありがとうございます。user.urlto_relative_uri 経由に揃えてから Octocrab::get に渡すよう c800327 で修正しました。

具体的には、http::Uri::from_str(user.url.as_str())http::Uri に変換してから to_relative_uri で scheme と authority を落とし、文字列化して get に渡しています。path は /api/v3/users/... の形で残るため、 BaseUriLayer は既に base_uri.path() (/api/v3) で始まる path を二重適用しない ( https://github.com/XAMPPRocky/octocrab/blob/v0.49.7/src/service/middleware/base_uri.rs#L91-L96 ) ので、最終的な URL は正しく https://git.example.com/api/v3/users/... のままになります。

Copy link
Copy Markdown

@n01e0 n01e0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

わいわい

@zaimy
Copy link
Copy Markdown
Contributor Author

zaimy commented Apr 24, 2026

レビューありがとうございました!!

@zaimy zaimy merged commit 2810bb3 into master Apr 24, 2026
1 check passed
@zaimy zaimy deleted the app-auth branch April 24, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants