はじめに
このチュートリアルでは、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 の任意のページの右上隅にある、自分のプロファイル写真をクリックします。
- アカウント設定にアクセスしてください。
- 個人用アカウントが所有するアプリの場合は、[設定] をクリックします。
- 組織が所有するアプリの場合:
- [自分の組織] をクリックします。
- 組織の右側にある [設定] をクリックします。
- 左側のサイドバーで [ 開発者設定] をクリックします。
- 左側のサイドバーで、 [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 end
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
-
ファイルの末尾に、エントリ ポイント関数を呼び出す次の行を追加します。 チュートリアルで後ほどこのファイルにさらに関数を追加するときも、この関数呼び出しをファイルの末尾にしておく必要があります。
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 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
-
必要に応じて、進行状況をチェックします。
ここまでで、
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 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
-
次の
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) 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
-
次の
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) 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
-
次の
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 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
-
次の
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!" 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
-
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 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
-
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
に追加します。 この関数は、/user
REST 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"]}" 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
-
トークンが有効期限切れになったり取り消されたりしたケースを処理するように、
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 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
-
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 end
def 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 を作成するためのベスト プラクティス」をご覧ください。