はじめに
このチュートリアルでは、GitHub App を利用してコマンド ライン インターフェイス (CLI) を構築する方法と、デバイス フローを使ってアプリ用のユーザー アクセス トークンを生成する方法について説明します。
CLI には、次の 3 つのコマンドがあります。
-
`help`: 使用手順を表示します。 -
`login`: ユーザーに代わってアプリが API 要求を行うために使用できるユーザー アクセス トークンを生成します。 -
`whoami`: ログインしているユーザーに関する情報を返します。
このチュートリアルでは Ruby を使いますが、任意のプログラミング言語で CLI を記述し、デバイス フローを使ってユーザー アクセス トークンを生成できます。
これらのステップを実行してください:
デバイス フローとユーザー アクセス トークンについて
この CLI では、デバイス フローを使ってユーザーを認証し、ユーザー アクセス トークンを生成します。 その後、CLI は、そのユーザー アクセス トークンを使って、認証されたユーザーの代わりに API 要求を行うことができます。
アプリのアクションをユーザーの属性にする場合は、アプリでユーザー アクセス トークンを使う必要があります。 詳しくは、「ユーザーに代わってGitHub アプリで認証する」をご覧ください。
GitHub App 用のユーザー アクセス トークンを生成するには、Web アプリケーション フローとデバイス フローの 2 つの方法があります。 アプリがヘッドレスの場合、または Web インターフェイスにアクセスできない場合は、デバイス フローを使ってユーザー アクセス トークンを生成する必要があります。 たとえば、CLI ツール、シンプルな Raspberry Pis、デスクトップ アプリケーションでは、デバイス フローを使う必要があります。 アプリが Web インターフェイスにアクセスできる場合は、代わりに Web アプリケーション フローを使う必要があります。 詳細については、「GitHub アプリのユーザー アクセス トークンの生成」および「GitHub アプリを使用して [GitHub でログイン] ボタンをビルドする」を参照してください。
前提条件
このチュートリアルでは、GitHub App を既に登録済みであることを前提としています。 GitHub App の登録の詳細については、「GitHub アプリの登録」を参照してください。
このチュートリアルを始める前に、アプリでデバイス フローを有効にする必要があります。 アプリでデバイス フローを有効にする方法の詳細については、「GitHub アプリの登録の変更」を参照してください。
このチュートリアルは、読者が Ruby の基礎を理解しているものとして書かれています。 詳しくは、Ruby の Web サイトをご覧ください。
クライアント ID を取得する
デバイス フローを使ってユーザー アクセス トークンを生成するには、アプリのクライアント ID が必要です。
- GitHub の任意のページの右上隅にある、自分のプロフィール写真をクリックします。
- アカウント設定にアクセスしてください。
- 個人用アカウントが所有するアプリの場合は、[設定] をクリックします。
- 組織が所有するアプリの場合:
- [自分の組織] をクリックします。
- 組織の右側にある [設定] をクリックします。
- 左側のサイドバーの [Developer settings] をクリックします。
- 左側のサイドバーで、 [GitHub Apps] をクリックします。
- 作業したい GitHub App の横にある [編集] を選びます。
- アプリの設定ページで、ご自分のアプリのクライアント ID を見つけます。 このチュートリアルで後ほどそれを使います。 クライアント ID は、アプリ ID とは異なることに注意してください。
CLI を記述する
以下の手順では、CLI を構築し、デバイス フローを使ってユーザー アクセス トークンを取得します。 スキップして最終的なコードに進むには、「完全なコードの例」を参照してください。
セットアップ
-
ユーザー アクセス トークンを生成するコードを保持する Ruby ファイルを作成します。 このチュートリアルでは、ファイルに
app_cli.rbという名前を付けます。 -
ターミナルで、
app_cli.rbが格納されているディレクトリから次のコマンドを実行して、app_cli.rb実行可能ファイルを作成します。Text chmod +x app_cli.rb
chmod +x app_cli.rb -
次の行を
app_cli.rbの先頭に追加して、Ruby インタープリターを使ってスクリプトを実行する必要があることを示します。Ruby #!/usr/bin/env ruby
#!/usr/bin/env ruby -
`app_cli.rb` の先頭の `#!/usr/bin/env ruby` の後に、これらの依存関係を追加します。Ruby require "net/http" require "json" require "uri" require "fileutils"
require "net/http" require "json" require "uri" require "fileutils"これらはすべて Ruby 標準ライブラリの一部であるため、gem をインストールする必要はありません。
-
エントリ ポイントとして機能する次の
main関数を追加します。 この関数には、指定されたコマンドに応じて異なるアクションを実行するcaseステートメントが含まれます。 このcaseステートメントを後で拡張します。Ruby def main case ARGV[0] when "help" puts "`help` is not yet defined" when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command `#{ARGV[0]}`" end enddef main case ARGV[0] when "help" puts "`help` is not yet defined" when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command `#{ARGV[0]}`" end end -
ファイルの末尾に、エントリ ポイント関数を呼び出す次の行を追加します。 チュートリアルで後ほどこのファイルにさらに関数を追加するときも、この関数呼び出しをファイルの末尾にしておく必要があります。
Ruby main
main -
必要に応じて、進行状況をチェックします。
ここまでで、
app_cli.rbは次のようになっています。Ruby #!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" def main case ARGV[0] when "help" puts "`help` is not yet defined" when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command `#{ARGV[0]}`" end end main#!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" def main case ARGV[0] when "help" puts "`help` is not yet defined" when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command `#{ARGV[0]}`" end end mainターミナルで、
app_cli.rbが保存されているディレクトリから、./app_cli.rb helpを実行します。 次のように出力されます。`help` is not yet definedまた、コマンドを指定せずに、または処理されないコマンドを指定して、スクリプトをテストすることもできます。 たとえば、
./app_cli.rb create-issueでは次のように出力されるはずです。Unknown command `create-issue`
`help` コマンドを追加する
-
次の
help関数をapp_cli.rbに追加します。 現在、help関数は、この CLI が 1 つのコマンド "help" を受け取ることをユーザーに伝える行を出力します。 後でこのhelp関数を拡張します。Ruby def help puts "usage: app_cli <help>" end
def help puts "usage: app_cli <help>" end -
`main` コマンドが指定されたら `help` 関数を呼び出すように `help` 関数を更新します。Ruby def main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end enddef main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end -
必要に応じて、進行状況をチェックします。
ここまでで、
app_cli.rbは次のようになっています。main関数の呼び出しがファイルの末尾にある限り、関数の順序は関係ありません。Ruby #!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" def help puts "usage: app_cli <help>" end def main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end main#!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" def help puts "usage: app_cli <help>" end def main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end mainターミナルで、
app_cli.rbが保存されているディレクトリから、./app_cli.rb helpを実行します。 次のように出力されます。usage: app_cli <help>
`login` コマンドを追加する
`login` コマンドは、デバイス フローを実行してユーザー アクセス トークンを取得します。 詳しくは、「[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-device-flow-to-generate-a-user-access-token)」をご覧ください。
-
ファイルの先頭近くにある
requireステートメントの後に、CLIENT_IDでの定数として GitHub App のapp_cli.rbを追加します。 アプリのクライアント ID の検索の詳細については、「クライアント ID を取得する」を参照してください。YOUR_CLIENT_IDは、実際のアプリのクライアント ID に置き換えます。Ruby CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_ID="YOUR_CLIENT_ID" -
次の
parse_response関数をapp_cli.rbに追加します。 この関数は、GitHub REST API からの応答を解析します。 応答の状態が200 OKまたは201 Createdの場合、関数は解析された応答本文を返します。 それ以外の場合、関数は応答と本文を出力して、プログラムを終了します。Ruby def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) else puts response puts response.body exit 1 end enddef parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) else puts response puts response.body exit 1 end end -
次の
request_device_code関数をapp_cli.rbに追加します。 この関数は、POSTにhttp(s)://HOSTNAME/login/device/code要求を行って、応答を返します。Ruby def request_device_code uri = URI("http(s)://HOSTNAME/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) enddef request_device_code uri = URI("http(s)://HOSTNAME/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end -
次の
request_token関数をapp_cli.rbに追加します。 この関数は、POSTにhttp(s)://HOSTNAME/login/oauth/access_token要求を行って、応答を返します。Ruby def request_token(device_code) uri = URI("http(s)://HOSTNAME/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) enddef request_token(device_code) uri = URI("http(s)://HOSTNAME/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end -
次の
poll_for_token関数をapp_cli.rbに追加します。 この関数は、GitHub がhttp(s)://HOSTNAME/login/oauth/access_tokenパラメーターではなくaccess_tokenパラメーターで応答するまで、指定された間隔でerrorへのポーリングを行います。 その後、ユーザー アクセス トークンをファイルに書き込み、ファイルに対するアクセス許可を制限します。Ruby def poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end enddef poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end end -
次の
login関数を追加します。この関数では、次の処理を実行します。
-
`request_device_code` 関数を呼び出して、応答から `verification_uri`、`user_code`、`device_code`、`interval` の各パラメーターを取得します。 - 前のステップの
user_codeを入力するようユーザーに求めます。 -
`poll_for_token` を呼び出して、GitHub からのアクセス トークンを取得するためにポーリングを行います。 - 認証が成功したことをユーザーに知らせます。
Ruby def login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" enddef login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" end -
-
`main` コマンドが指定されたら `login` 関数を呼び出すように `login` 関数を更新します。Ruby def main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end enddef main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end -
`help` コマンドを含むように `login` 関数を更新します。Ruby def help puts "usage: app_cli <login | help>" end
def help puts "usage: app_cli <login | help>" end -
必要に応じて、進行状況をチェックします。
これで、
app_cli.rbは次のようになります。YOUR_CLIENT_IDはアプリのクライアント ID です。main関数の呼び出しがファイルの末尾にある限り、関数の順序は関係ありません。Ruby #!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" CLIENT_ID="YOUR_CLIENT_ID" def help puts "usage: app_cli <login | help>" end def main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) else puts response puts response.body exit 1 end end def request_device_code uri = URI("http(s)://HOSTNAME/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def request_token(device_code) uri = URI("http(s)://HOSTNAME/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end end def login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" end main#!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" CLIENT_ID="YOUR_CLIENT_ID" def help puts "usage: app_cli <login | help>" end def main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) else puts response puts response.body exit 1 end end def request_device_code uri = URI("http(s)://HOSTNAME/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def request_token(device_code) uri = URI("http(s)://HOSTNAME/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end end def login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" end main-
ターミナルで、
app_cli.rbが保存されているディレクトリから、./app_cli.rb loginを実行します。 出力は次のようになります。 コードは毎回異なります。Please visit: http(s)://HOSTNAME/login/device and enter code: CA86-8D94 -
ブラウザーで http(s)://HOSTNAME/login/device に移動し、前のステップのコードを入力して、 [続行] をクリックします。
-
GitHub で、アプリの承認を求めるページが表示されます。 [承認] ボタンをクリックします。
-
ターミナルに "Successfully authenticated!" と表示されます。
-
`whoami` コマンドを追加する
アプリでユーザー アクセス トークンを生成できるようになったので、ユーザーに代わって API 要求を行うことができます。 認証されたユーザーのユーザー名を取得する whoami コマンドを追加します。
-
次の
whoami関数をapp_cli.rbに追加します。 この関数は、/userREST API エンドポイントでユーザーに関する情報を取得します。 ユーザー アクセス トークンに対応するユーザー名を出力します。.tokenファイルが見つからなかった場合は、login関数を実行するようユーザーに求めます。Ruby def whoami uri = URI("http(s)://HOSTNAME/api/v3/user") begin token = File.read("./.token").strip rescue Errno::ENOENT => e puts "You are not authorized. Run the `login` command." exit 1 end response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| body = {"access_token" => token}.to_json headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"} http.send_request("GET", uri.path, body, headers) end parsed_response = parse_response(response) puts "You are #{parsed_response["login"]}" enddef whoami uri = URI("http(s)://HOSTNAME/api/v3/user") begin token = File.read("./.token").strip rescue Errno::ENOENT => e puts "You are not authorized. Run the `login` command." exit 1 end response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| body = {"access_token" => token}.to_json headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"} http.send_request("GET", uri.path, body, headers) end parsed_response = parse_response(response) puts "You are #{parsed_response["login"]}" end -
トークンが有効期限切れになったり取り消されたりしたケースを処理するように、
parse_response関数を更新します。 ここで、401 Unauthorized応答を受け取った場合、CLI はユーザーにloginコマンドの実行を求めます。Ruby def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) when Net::HTTPUnauthorized puts "You are not authorized. Run the `login` command." exit 1 else puts response puts response.body exit 1 end enddef parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) when Net::HTTPUnauthorized puts "You are not authorized. Run the `login` command." exit 1 else puts response puts response.body exit 1 end end -
`main` コマンドが指定されたら `whoami` 関数を呼び出すように `whoami` 関数を更新します。Ruby def main case ARGV[0] when "help" help when "login" login when "whoami" whoami else puts "Unknown command #{ARGV[0]}" end enddef main case ARGV[0] when "help" help when "login" login when "whoami" whoami else puts "Unknown command #{ARGV[0]}" end end -
`help` コマンドを含むように `whoami` 関数を更新します。Ruby def help puts "usage: app_cli <login | whoami | help>" end
def help puts "usage: app_cli <login | whoami | help>" end -
コードを次のセクションの完全なコード例に照らしてチェックします。 コードをテストするには、完全なコード例の後の「テスト」セクションで説明されている手順のようにします。
完全なコード例
次に、前のセクションで概要を説明したコードの完全な例を示します。
YOUR_CLIENT_ID は、ご自分のアプリのクライアント ID に置き換えます。
#!/usr/bin/env ruby
require "net/http"
require "json"
require "uri"
require "fileutils"
CLIENT_ID="YOUR_CLIENT_ID"
def help
puts "usage: app_cli <login | whoami | help>"
end
def main
case ARGV[0]
when "help"
help
when "login"
login
when "whoami"
whoami
else
puts "Unknown command #{ARGV[0]}"
end
end
def parse_response(response)
case response
when Net::HTTPOK, Net::HTTPCreated
JSON.parse(response.body)
when Net::HTTPUnauthorized
puts "You are not authorized. Run the `login` command."
exit 1
else
puts response
puts response.body
exit 1
end
end
def request_device_code
uri = URI("http(s)://HOSTNAME/login/device/code")
parameters = URI.encode_www_form("client_id" => CLIENT_ID)
headers = {"Accept" => "application/json"}
response = Net::HTTP.post(uri, parameters, headers)
parse_response(response)
end
def request_token(device_code)
uri = URI("http(s)://HOSTNAME/login/oauth/access_token")
parameters = URI.encode_www_form({
"client_id" => CLIENT_ID,
"device_code" => device_code,
"grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
})
headers = {"Accept" => "application/json"}
response = Net::HTTP.post(uri, parameters, headers)
parse_response(response)
end
def poll_for_token(device_code, interval)
loop do
response = request_token(device_code)
error, access_token = response.values_at("error", "access_token")
if error
case error
when "authorization_pending"
# The user has not yet entered the code.
# Wait, then poll again.
sleep interval
next
when "slow_down"
# The app polled too fast.
# Wait for the interval plus 5 seconds, then poll again.
sleep interval + 5
next
when "expired_token"
# The `device_code` expired, and the process needs to restart.
puts "The device code has expired. Please run `login` again."
exit 1
when "access_denied"
# The user cancelled the process. Stop polling.
puts "Login cancelled by user."
exit 1
else
puts response
exit 1
end
end
File.write("./.token", access_token)
# Set the file permissions so that only the file owner can read or modify the file
FileUtils.chmod(0600, "./.token")
break
end
end
def login
verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval")
puts "Please visit: #{verification_uri}"
puts "and enter code: #{user_code}"
poll_for_token(device_code, interval)
puts "Successfully authenticated!"
end
def whoami
uri = URI("http(s)://HOSTNAME/api/v3/user")
begin
token = File.read("./.token").strip
rescue Errno::ENOENT => e
puts "You are not authorized. Run the `login` command."
exit 1
end
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
body = {"access_token" => token}.to_json
headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"}
http.send_request("GET", uri.path, body, headers)
end
parsed_response = parse_response(response)
puts "You are #{parsed_response["login"]}"
end
main
#!/usr/bin/env ruby
require "net/http"
require "json"
require "uri"
require "fileutils"
CLIENT_ID="YOUR_CLIENT_ID"
def help
puts "usage: app_cli <login | whoami | help>"
end
def main
case ARGV[0]
when "help"
help
when "login"
login
when "whoami"
whoami
else
puts "Unknown command #{ARGV[0]}"
end
end
def parse_response(response)
case response
when Net::HTTPOK, Net::HTTPCreated
JSON.parse(response.body)
when Net::HTTPUnauthorized
puts "You are not authorized. Run the `login` command."
exit 1
else
puts response
puts response.body
exit 1
end
end
def request_device_code
uri = URI("http(s)://HOSTNAME/login/device/code")
parameters = URI.encode_www_form("client_id" => CLIENT_ID)
headers = {"Accept" => "application/json"}
response = Net::HTTP.post(uri, parameters, headers)
parse_response(response)
end
def request_token(device_code)
uri = URI("http(s)://HOSTNAME/login/oauth/access_token")
parameters = URI.encode_www_form({
"client_id" => CLIENT_ID,
"device_code" => device_code,
"grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
})
headers = {"Accept" => "application/json"}
response = Net::HTTP.post(uri, parameters, headers)
parse_response(response)
end
def poll_for_token(device_code, interval)
loop do
response = request_token(device_code)
error, access_token = response.values_at("error", "access_token")
if error
case error
when "authorization_pending"
# The user has not yet entered the code.
# Wait, then poll again.
sleep interval
next
when "slow_down"
# The app polled too fast.
# Wait for the interval plus 5 seconds, then poll again.
sleep interval + 5
next
when "expired_token"
# The `device_code` expired, and the process needs to restart.
puts "The device code has expired. Please run `login` again."
exit 1
when "access_denied"
# The user cancelled the process. Stop polling.
puts "Login cancelled by user."
exit 1
else
puts response
exit 1
end
end
File.write("./.token", access_token)
# Set the file permissions so that only the file owner can read or modify the file
FileUtils.chmod(0600, "./.token")
break
end
end
def login
verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval")
puts "Please visit: #{verification_uri}"
puts "and enter code: #{user_code}"
poll_for_token(device_code, interval)
puts "Successfully authenticated!"
end
def whoami
uri = URI("http(s)://HOSTNAME/api/v3/user")
begin
token = File.read("./.token").strip
rescue Errno::ENOENT => e
puts "You are not authorized. Run the `login` command."
exit 1
end
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
body = {"access_token" => token}.to_json
headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"}
http.send_request("GET", uri.path, body, headers)
end
parsed_response = parse_response(response)
puts "You are #{parsed_response["login"]}"
end
main
テスト
このチュートリアルでは、アプリ コードが app_cli.rb という名前のファイルに格納されているものと想定しています。
-
ターミナルで、
app_cli.rbが保存されているディレクトリから、./app_cli.rb helpを実行します。 出力は次のようになります。usage: app_cli <login | whoami | help> -
ターミナルで、
app_cli.rbが保存されているディレクトリから、./app_cli.rb loginを実行します。 出力は次のようになります。 コードは毎回異なります。Please visit: http(s)://HOSTNAME/login/device and enter code: CA86-8D94 -
ブラウザーで http(s)://HOSTNAME/login/device に移動し、前のステップのコードを入力して、 [続行] をクリックします。
-
GitHub で、アプリの承認を求めるページが表示されます。 [承認] ボタンをクリックします。
-
ターミナルに "Successfully authenticated!" と表示されます。
-
ターミナルで、
app_cli.rbが保存されているディレクトリから、./app_cli.rb whoamiを実行します。 次のような出力が表示されます。octocatはユーザー名です。You are octocat -
エディターで
.tokenファイルを開き、トークンを変更します。 これで、トークンは無効になります。 -
ターミナルで、
app_cli.rbが保存されているディレクトリから、./app_cli.rb whoamiを実行します。 出力は次のようになります。You are not authorized. Run the `login` command. -
`.token` ファイルを削除します。 -
ターミナルで、
app_cli.rbが保存されているディレクトリから、./app_cli.rb whoamiを実行します。 出力は次のようになります。You are not authorized. Run the `login` command.
次の手順
アプリのニーズに合わせてコードを調整する
このチュートリアルでは、デバイス フローを使ってユーザー アクセス トークンを生成する CLI を記述する方法を示しました。 追加のコマンドを受け取るように、この CLI を拡張できます。 たとえば、問題を開く create-issue コマンドを追加できます。 行う API 要求でアプリに追加のアクセス許可が必要な場合は、忘れずにアプリのアクセス許可を更新してください。 詳しくは、「GitHub アプリのアクセス許可の選択」をご覧ください。
トークンを安全に保存する
このチュートリアルでは、ユーザー アクセス トークンを生成し、それをローカル ファイルに保存します。 このファイルをコミットしたり、トークンを公開したりしないでください。
デバイスによっては、異なる方法でトークンを保存できます。 デバイスにトークンを格納するためのベスト プラクティスを確認する必要があります。
詳しくは、「GitHub アプリを作成するためのベスト プラクティス」をご覧ください。
ベスト プラクティスに従う
GitHub App に関するベスト プラクティスに従うようにする必要があります。 詳しくは、「GitHub アプリを作成するためのベスト プラクティス」をご覧ください。