3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub App の Device Flowでスコープを絞った短命なGitHubトークンを生成する

3
Last updated at Posted at 2026-06-19

はじめに

GitHubの公式CLIであるghコマンドはべんりですが、非常に強い権限を持っているので攻撃する側から見ても魅力的です。特に gh auth token コマンドを実行するだけで簡単にGitHubトークンを抜けることは、もはや公然の秘密というか、みんな見て見ぬふりをしている大きな穴です。さらに都合の悪いことに、GitHubの公式CLIのOAuth Appで認証すると、このトークンには repo 権限があるので、自分がアクセス可能なプライベートリポジトリを読み書きでき、有効期限も設定されていないので、トークンが漏洩した場合の影響が大きくなりがちです。最近のマルウェアは個人のローカル端末を狙う攻撃が増えてきているので、GitHubトークンを抜かれた場合のダメージを如何に最小化するかというのは重要な課題です。

最初に思いつく代替案は、GitHub Fine-grained PATを使うことです。細かくスコープと有効期限を絞れるのでよい選択肢だと思いますが、トークンを生成するAPIが生えておらずローテーションが面倒です。人類は怠惰なので期限を長めに設定しがちです。また会社で管理しているGitHub orgにアクセスする場合は、orgの管理者側で有効期限の上限を設定したりスコープをレビューすることは可能なものの、逐一妥当性をチェックするのは管理者側の負担も無視できません。

GitHub Actionsから他のリポジトリのGitHub APIを叩いたことがある人なら、GitHub Appのインストレーショントークンを使うという案も思いつくかもしれません。しかしながらこの方法は、マシンユーザからのアクセスに使うことを想定しており、クライアントシークレットの管理が必要となることや、操作の主体がGitHub App=botユーザになってしまうので、複数の開発者がローカル端末で共有するのには不向きです。個人利用ならさておき、開発者の人数分だけAppを作ろうとするとスケールしません。

第3の選択肢として、GitHub App の Device Flowを使ってユーザアクセストークン(User-to-Server Token)を生成するという方法があります。この方法であればクライアントシークレットの管理も不要で、操作の主体もユーザになるので、開発者がローカル端末で使うのに都合がよいです。GitHub Appのユーザアクセストークンは有効期限が8時間で、スコープもOAuth Appよりも細かい粒度で絞れます。

この記事では、GitHub App の Device FlowでGitHubトークンを生成する方法について、curlを使った素朴な方法から始めて仕組みの理解を深めつつ、実務上ghコマンドやgitコマンドなどと組み合わせて使うのに便利なghtknというツールの使い方を補足します。

GitHub App の Device Flow とは

GitHub App の Device Flow とは、RFC8628: OAuth 2.0 Device Authorization Grant を使ってGitHubトークンを払い出す仕組みです。

OAuth 2.0のDevice Flowは、元々ブラウザにアクセスできないようなデバイスが、ユーザの認可を得てリソースにアクセスするための仕組みです。ここでいう「デバイス」というのは、たとえばネット接続可能なスマート家電のようなものを想定して設計されたものですが、OAuthクライアントであるデバイスは、ユーザのブラウザに直接アクセスできないで、ユーザに指定のURLにアクセスしてワンタイムパスワードを入力してもらうことで、間接的に許可を得て、認可サーバからアクセストークンを得ます。

以下は、RFC8628から抜粋したデバイスフローの概要図です。

      +----------+                                +----------------+
      |          |>---(A)-- Client Identifier --->|                |
      |          |                                |                |
      |          |<---(B)-- Device Code,      ---<|                |
      |          |          User Code,            |                |
      |  Device  |          & Verification URI    |                |
      |  Client  |                                |                |
      |          |  [polling]                     |                |
      |          |>---(E)-- Device Code       --->|                |
      |          |          & Client Identifier   |                |
      |          |                                |  Authorization |
      |          |<---(F)-- Access Token      ---<|     Server     |
      +----------+   (& Optional Refresh Token)   |                |
            v                                     |                |
            :                                     |                |
           (C) User Code & Verification URI       |                |
            :                                     |                |
            v                                     |                |
      +----------+                                |                |
      | End User |                                |                |
      |    at    |<---(D)-- End user reviews  --->|                |
      |  Browser |          authorization request |                |
      +----------+                                +----------------+

                    Figure 1: Device Authorization Flow

デバイスフローの流れをざっくり説明すると、

(A) クライアントは、クライアントIDを認可サーバに送信する。
(B) 認可サーバは、デバイスコード、ユーザコード、検証URLを返す。
(C) クライアントは、ユーザに「検証用URLにユーザコードを入力」するように依頼する。
(D) 認可サーバは、ユーザを認証し、ユーザコードを検証し、リクエストの許可を求める。
(E) クライアントは、クライアントIDとデバイスコードを認可サーバに送信し、ユーザの許可が得られるまでリトライする。
(F) 認可サーバは、クライアントIDとデバイスコードを検証し、ユーザの許可が得られればアクセストークンを返す。

で、話をGitHubに戻すと、このブラウザに直接アクセスできないクライアントデバイスというのが、GitHub APIを叩きたいアプリケーションで、CLIツールのようなヘッドレスで使うのに都合がよく、GitHub Appの認証方法としてDevice Flowを使うことができます。

念のため混同しないように補足しておきますが、OAuth 2.0のDevice Flow自体は手続きの流れを規定しているだけで、GitHubには「OAuth App」と「GitHub App」という似たような機能があり、どちらもDevice Flowが使えます。歴史的な経緯で(?)GitHub公式CLIは「OAuth App」としてDevice Flowを使っていますが、「OAuth App」はスコープの粒度が荒く、有効期限も設定できません。
一方、後発のGitHub Appはスコープを細かい粒度で指定でき、Device Flowで認証すると、有効期限が8時間だけのアクセストークンが得られます。GitHub AppにはDevice Flowの他に、独自プロトコルでインストレーショントークンを発行する方法もありますが、Device Flowを使ったユーザアクセストークンは操作の主体がユーザとなるので、ユーザが持っている権限以上のことはできず、個人がローカル端末で使うのに都合がよいです。用途に合わせて権限の異なるGitHub Appを複数作成することで、読み取り専用のトークンと書き込み可能なトークンを使い分けることもできます。

ちなみに有効期限8時間は固定で、現状は期限を変更する設定はありません。8時間を短命というにはちょっと長い気もしますが、Fine-grained PATの最小期限は1日ですし、OAuthだと無期限なことを考えれば最短です。トークンが漏洩した場合の影響を最小化するという観点では、現状一番マシな選択肢でしょう。

やってみた

事前準備

ghコマンドがログインした状態だと、稼働確認が分かりづらいので、 gh auth logout してから試すことをオススメします。

$ gh auth logout
$ gh auth status
You are not logged into any GitHub hosts. To log in, run: gh auth login

ghコマンドは環境変数 GH_TOKEN があればそれを優先的に見てくれるので、 gh auth login していなくても使えます。

GitHub Appの作成

適当なGitHub Appを作成します。個人設定の場合は以下にあります。
「Settings」 > 「Developer Settings」 > 「GitHub Apps」 > 「New GitHub App」
https://github.com/settings/apps

設定は以下のとおりとしました。ポイントは「Enable Device Flow」のチェックを入れることです。

  • GitHub App name: minamijoyo-ghdevice-none(適当な名前を付ける)
  • Homepage URL: https://github.com/minamijoyo (適当に埋めておく)
  • Identifying and authorizing users:
    • Expire user authorization tokens: on (デフォルトでチェックが付いてる)
    • Enable Device Flow: on(チェックを入れる)
  • Webhook:
    • Active: off(チェックを外す)
  • Permissions: (とりあえずデフォルトのまま何も付けない)
  • Where can this GitHub App be installed?: Only on this account (デフォルトのまま)

github.com_settings_apps_new.png

GitHub Appの名前についての補足ですが、インストール可能な範囲を「Only on this account」に制限しても、App名は公開されます。グローバルでユニークな名前にすることと、外から見られても問題ない名前にする必要があります。GitHub orgやEnterprise内にAppを作ってインストール範囲を制限した場合でも同じです。

Permissionsは検証のため、とりあえずデフォルトのまま何も付けていないですが、これだけでもpublicリポジトリにはアクセスできますし、レートリミット制限の緩和にもなるので、一応意味はあります。
他にもRead-only用のAppや、Write用のAppなど権限を分けたAppを用意するとよいでしょう。
一般的なコードを書く作業では、contents, issues, pull-requestsの読み書きがあれば足りることが多いと思うので、ここでは以下のような権限の異なるGitHub Appを用意しました。

  • minamijoyo-ghdevice-none: (権限なし)
  • minamijoyo-ghdevice-read:
    • contents:read
    • issues:read
    • pull-requests:read
    • metadata:read
  • minamijoyo-ghdevice-write:
    • contents:write
    • issues:write
    • pull-requests:write
    • metadata:read

補足として、GitHub Appになんらかの権限を付与すると、metadata:readの権限も自動で付いてきますが仕様です。actionsを叩きたいとか、他のAPIを使いたいケースはユースケースごとにAppを分けるとよいかもしれません。

GitHub Appを作成するとclient_idが払い出されます。以降で使うので控えておきます。
client_id自体はシークレットではありませんが、インストール範囲を制限したprivateなGitHub Appの場合は公開情報でもありません。client_idだけでアクセストークンは取得できませんが、社内で共有する場合は社内限の取り扱いが妥当かなと思います。

次にGitHub Appをインストールします。リポジトリ単位に設定するか、ユーザスコープ or orgスコープでまとめてインストールすることもできます。
「Settings」 > 「Developer Settings」 > 「GitHub Apps」から先ほど作成したAppを選択し、
https://github.com/settings/apps

ここではユーザスコープで全リポジトリにインストールします。
「Install App」 > 「All Repositories」 > 「Install」

補足として、GitHub App の Device Flowで払い出されるユーザアクセストークン(User-to-Server Token)は、GitHub Appの権限とユーザの権限の積集合の権限しか持っていないので、org内の全リポジトリに一括でインストールしても、ユーザがアクセスできないリポジトリにはアクセスできません。

逆に言うと、Appをインストールしていないリポジトリにもアクセスできないので、Appのインストールの有無で権限の違いを確認したい場合は、特定のリポジトリにだけインストールしてみて下さい。

curlでナマのAPIを叩いてみる

仕組みを理解しないと使えないタイプの人間なので、まずはcurlでナマのAPIを叩いてみます。

まず、ターミナルからcurlコマンドで https://github.com/login/device/codeclient_id をPOSTすると、device_codeuser_codeverification_uri が返ってきます。

$ curl -s -X POST https://github.com/login/device/code \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "xxxx"}' \
  | jq .

{
  "device_code": "xxxx",
  "user_code": "XXXX-XXXX",
  "verification_uri": "https://github.com/login/device",
  "expires_in": 899,
  "interval": 5
}

次に、ブラウザで https://github.com/login/device を開いて、GitHubにログインして認証します。既にログイン済みの場合は、GitHubのアカウント選択画面が出るので、アカウントを選択します。

image.png

ユーザコードを入力する画面が出るので、先ほどのレスポンスにあったユーザコードを入力します。

image.png

「Authorize」ボタンを押して、認可リクエストを承認します。(この画面に要求するスコープの一覧が出ないのはちょっと微妙な仕様ですね)

image.png

ターミナルに戻って https://github.com/login/oauth/access_tokenclient_iddevice_code をPOSTします。このとき、 grant_type"urn:ietf:params:oauth:grant-type:device_code" を指定します。

$ curl -s -X POST https://github.com/login/oauth/access_token \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "xxxx", "device_code": "xxxx", "grant_type": "urn:ietf:params:oauth:grant-type:device_code"}' \
  | jq .

{
  "access_token": "ghu_xxxx",
  "expires_in": 28800,
  "refresh_token": "ghr_xxxx",
  "refresh_token_expires_in": 15811200,
  "token_type": "bearer",
  "scope": ""
}

これで、レスポンスにaccess_tokenとrefresh_tokenが得られます。
expires_inの単位は秒なので、アクセストークンの期限は8時間、リフレッシュトークンの期限は183日です。リフレッシュトークンの期限がおよそ半年もあり、現状有効期限を変更できないのがちょっと気になりますが、リフレッシュトークンは使うと、古いアクセストークンとリフレッシュトークンは無効になります。

curlでこのアクセストークンが有効なことを確認してみましょう。

$ curl -s https://api.github.com/user \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: token ghu_xxxx" \
  | jq -r .login

minamijoyo

ghコマンドは環境変数 GH_TOKEN が設定されていればそれを使うので、アクセストークンを指定すれば使えます。

$  GH_TOKEN=ghu_xxxx gh issue list

gh issue list コマンドはpublicリポジトリであれば権限なしで叩けますが、privateリポジトリの場合は、事前に issues:read 権限を持ったGitHub Appを対象のリポジトリにインストールしておく必要があります。権限やリポジトリを変えてアクセス制御が意図したとおりに機能しているか試してみて下さい。

ここまで試せば分かると思いますが、この使い方ではGitHub Appというのはただの権限セットの定義でしかありません。なんらか常時起動するサーバが必要なわけでもありません。用途に合わせて複数用意して使い分けるとよいでしょう。

3rd-partyツールによるトークン生成

残念ながら本稿執筆時点では、任意のGitHub AppのDevice Flowのトークンを生成するGitHub公式のクライアント実装は存在しないので、3rd-partyツールの実装がいくつかあります。

私はプラグインマネージャにmiseを使っているので、mise v2026.05.06で実験的に追加されたGitHub AppのDevice Flowを使ったGitHubトークン生成機能が気になって試してみたのですが、本稿検証時点のmise v2026.6.10では、このトークンが ~/.local/state/mise/github-oauth-tokens.toml に平文で保存されてしまうという問題があり、まだexperimentalな扱いなので今後に期待です :expressionless:

以下ではちゃんとトークンを暗号化してOSのキーチェインに保存してくれるghtknを使ってみます。
3rd-partyは使えないポリシーの人は、アクセストークンの払い出し処理そのものはそんなに難しくないので、自作して車輪の再発明すればよいんじゃなかろうか。

ghtkn

ghtknは、GitHub AppのDevice Flowによるトークンの払い出しをしてくれるCLIツールです。単にアクセストークンを生成するだけではなく、Git Credential Helperとして振る舞うモードもあり、デフォルト設定でトークンは暗号化してOSのキーチェインに保存されます。

既に作者の@suzuki-shunsuke氏による解説記事があるので、そっちを読んでねで終わらせてよいんですが、

僭越ながら、私なりの使い方を以下でも簡単にまとめておきます。

インストール

ghtknはGo製のツールでビルド済みのバイナリがGitHub Releaseで配布されているので、いろいろな方法でインストール可能です。インストール方法は、公式ドキュメントを参照して下さい。
https://github.com/suzuki-shunsuke/ghtkn/blob/main/INSTALL.md

私はmiseで管理しているので、mise経由でインストールしました。

$ mise use -g ghtkn
$ ghtkn --version
ghtkn version 0.2.5

ghtknの設定

ghtkn init すると ~/.config/ghtkn/ghtkn.yaml に設定ファイルの雛形が生成されます。

$ ghtkn init

ここでは、GitHub Appを権限ごとに分けて作成し、それぞれのclient_idを登録します。

~/.config/ghtkn/ghtkn.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/ghtkn-go-sdk/refs/heads/main/json-schema/ghtkn.json
# ghtkn - https://github.com/suzuki-shunsuke/ghtkn
apps:
  - name: minamijoyo/none
    client_id: xxxx
  - name: minamijoyo/read
    client_id: xxxx
  - name: minamijoyo/write
    client_id: xxxx

ghtkn auth コマンドを実行するとブラウザが開くので、ユーザコードを入力して、アクセスを認可するとアクセストークンが払い出されます。

$ ghtkn auth minamijoyo/none
The application uses the device flow to generate your GitHub User Access Token.
Copy your one-time code: XXXX-XXXX
This code is valid until YYYY-MM-DDThh:mm:ss+09:00
Press Enter to open https://github.com/login/device in your browser (it opens automatically after 10 seconds)...

ghtkn get コマンドでトークンを取得します。

$ ENV GH_TOKEN=$(ghtkn get minamijoyo/none) gh issue list

ghコマンドをラップする

App名は引数でも指定できますが、環境変数 GHTKN_APP でも指定可能です。
direnvなどでディレクトリに応じて自動で GHTKN_APP を差し込む想定で、 適当なラッパースクリプトを書いてPATHを通しておくことで、 gh コマンドを差し替えます。

#!/usr/bin/env bash

set -eu

if [ -z "${GH_TOKEN:-}" ] && [ -z "${GITHUB_TOKEN:-}" ]; then
  GH_TOKEN="$(ghtkn get)"
  export GH_TOKEN
fi

exec $(mise which gh) "$@"

無限ループしないように注意して下さい。最後の行はインストール方法に依存しますが、絶対パスに読み替えてください。

Git Credential Helperとして使う

git コマンドをhttps経由で使っている場合、Git Credential Helperにもアクセストークンが埋まっています。これは普段あんまり意識していないかもしれませんが、 git credential fill コマンドを使うと簡単にトークンが抜けます。知ってましたか?気になる人は以下のコマンドを試してみて下さい。

$ git credential fill <<EOS
protocol=https
host=github.com

EOS
protocol=https
host=github.com
username=x-access-token
password=xxxx

Git Credential Helperをどのように設定しているかによりますが、GitHub公式のGCM(Git Credential Manager)を使っているか、あるいは gh コマンド経由で gh auth setup-git して gh auth git-credential を使っているかが多いかと思います。GCMのOAuthトークンを使っている場合、これも期限がないので差し替えます。
ghtkn git-credential コマンドでGit Credential Helperのように振る舞うモードがあるので、~/.gitconfig のcredentialセクションで以下のように設定します。

~/.gitconfig
[credential]
	helper =
	helper = !ghtkn git-credential
	useHttpPath = true

orgごとに GHTKN_APP を差し替えたい場合は、いくつか方法が考えられますが、たとえば以下のように即時関数で GHTKN_APP を差し込む処理を、 includeIf を使って出し分けします。

~/.gitconfig
[credential]
	helper =
	helper = "!f(){ ENV GHTKN_APP=minamijoyo/write ghtkn git-credential; };f"
	useHttpPath = true
[includeIf "gitdir:~/src/github.com/minamijoyo/"]
  path = ...

私は、既に includeIf でemailなどを差し替えていた流れでgitconfig側で吸収していますが、他に ~/.config/ghtkn/ghtkn.yamlgit_owner を指定する方法と、v0.2.6以降で環境変数 GHTKN_GIT_APP を使う方法があるようです。(未検証)

ユーザコードをクリップボードにコピペする

アクセストークンの有効期限が8時間で短いのはよいことなのですが、トークンの払い出しはApp単位なので、複数のAppを使い分けていると、1日数回認証が必要になり、ユーザコードをコピペするのも煩雑です。
本稿執筆時点ではghtknはリフレッシュトークンに対応していないので、ghtkn authの出力をパースして、ユーザコードをコピーするワンライナーを作成しておくとべんりそうです。
素朴に実装すると以下のようなイメージになりますが、これは意図したとおりに機能しません。

$ ghtkn auth | grep -oE "[A-Z0-9]{4}-[A-Z0-9]{4}" | pbcopy

これがなぜうまくいかないかと言うと、 ghtkn auth コマンドはユーザコードを出力したのちに、アクセストークンが払い出されるまでフォアグラウンドでブロックしてしまうので、 pbcopy コマンドは入力を待ち続けてしまい、バッファをクリップボードにフラッシュしないからです。
いろいろ試行錯誤した結果、正解は以下のとおりです。

~/.zshrc
alias ghauth='(){ ghtkn auth "$@" 2>&1 | tee >(grep -oE "[A-Z0-9]{4}-[A-Z0-9]{4}" --line-buffered | head -n1 | tr -d "\n" | pbcopy) }'

一応解説しておくと、ghtkn auth はユーザコードを標準エラー出力に書き込むので、 tee コマンドと >() プロセス置換を組み合わせて出力を複製しつつ、 grep --line-buffered でバッファを1行単位に出力し、 head -n1 で1行だけ読んだら早期終了してパイプを閉じ、下流の pbcopy にこれ以上入力がないことを伝えます。 tr -d "\n" で改行を取り除くのは、改行が入ってるとGitHub側のユーザコード入力画面に貼り付けができないからです。

これをzshのエイリアスや関数として ~/.zshrc に仕込んでおくと、ユーザコードがクリップボードにコピーされた状態でブラウザが立ち上がります。あとは貼り付けるだけの簡単なお仕事。

$ ghauth minamijoyo/none

不要になった公式のOAuth Appを無効化する

最後に不要になった公式のOAuth Appは忘れずに無効化しておきましょう。

「Settings」 > 「Applications」
https://github.com/settings/applications

「Authorized OAuth Apps」のタブで、「GitHub CLI」をRevokeします。
「Git Credential Manager」もOAuth Appで使っていたのであれば、同様にRevokeします。

まとめ

GitHub App の Device Flowでスコープを絞った短命なGitHubトークンを生成する方法について説明しました。
GitHub公式でもうちょっといいかんじになんとかして欲しい気持ちはありつつ、現状の技術的な制約の元で一番マシそうな解決策だと思うのでお試し下さい。
世界が少しでもセキュアになりますように :pray:

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?