はじめに
このチュートリアルでは、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 App を使って [Login with GitHub] ボタンを作成する」を参照してください。
前提条件
このチュートリアルでは、GitHub App を既に登録済みであることを前提としています。 GitHub App の登録の詳細については、「GitHub App の登録」を参照してください。
このチュートリアルを始める前に、アプリでデバイス フローを有効にする必要があります。 アプリでデバイス フローを有効にする方法の詳細については、「GitHub App 登録の変更」を参照してください。
このチュートリアルは、読者が Ruby の基礎を理解しているものとして書かれています。 詳しくは、Ruby の Web サイトをご覧ください。
クライアント ID を取得する
デバイス フローを使ってユーザー アクセス トークンを生成するには、アプリのクライアント ID が必要です。
-
GitHub の任意のページの右上隅にある、自分のプロフィール写真をクリックします。
-
アカウント設定にアクセスしてください。
- 個人用アカウントが所有するアプリの場合は、[設定] をクリックします。
- 組織が所有するアプリの場合:
- [自分の組織] をクリックします。
- 組織の右側にある [設定] をクリックします。
- Enterprise が所有するアプリの場合:
- [Enterprise 設定] をクリックします。
-
GitHub App 設定にアクセスしてください。
- 個人用アカウントまたは組織が所有するアプリの場合:
- 左側のサイドバーで、[ Developer settings] をクリックしてから、[GitHub Apps] をクリックします。
- Enterprise が所有するアプリの場合:
- 左側のサイドバーで、 [設定] をクリックし、 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 -
helpコマンドが指定されたらhelp関数を呼び出すようにmain関数を更新します。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 コマンドは、デバイス フローを実行してユーザー アクセス トークンを取得します。 詳しくは、「GitHub アプリのユーザー アクセス トークンの生成」をご覧ください。
-
ファイルの先頭近くにある
requireステートメントの後に、app_cli.rbでの定数として GitHub App のCLIENT_IDを追加します。 アプリのクライアント 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に追加します。 この関数は、http(s)://HOSTNAME/login/device/codeにPOST要求を行って、応答を返します。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に追加します。 この関数は、http(s)://HOSTNAME/login/oauth/access_tokenにPOST要求を行って、応答を返します。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 がerrorパラメーターではなくaccess_tokenパラメーターで応答するまで、指定された間隔でhttp(s)://HOSTNAME/login/oauth/access_tokenへのポーリングを行います。 その後、ユーザー アクセス トークンをファイルに書き込み、ファイルに対するアクセス許可を制限します。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 -
loginコマンドが指定されたらlogin関数を呼び出すようにmain関数を更新します。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 -
loginコマンドを含むようにhelp関数を更新します。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 -
whoamiコマンドが指定されたらwhoami関数を呼び出すようにmain関数を更新します。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 -
whoamiコマンドを含むようにhelp関数を更新します。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 App を作成するためのベスト プラクティス」をご覧ください。
ベスト プラクティスに従う
GitHub App に関するベスト プラクティスに従うようにする必要があります。 詳しくは、「GitHub App を作成するためのベスト プラクティス」をご覧ください。