はじめに
このチュートリアルでは、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
に追加します。 この関数は、https://github.com/login/device/code
にPOST
要求を行って、応答を返します。Ruby def request_device_code uri = URI("https://github.com/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("https://github.com/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
に追加します。 この関数は、https://github.com/login/oauth/access_token
にPOST
要求を行って、応答を返します。Ruby def request_token(device_code) uri = URI("https://github.com/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("https://github.com/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
パラメーターで応答するまで、指定された間隔でhttps://github.com/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("https://github.com/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("https://github.com/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("https://github.com/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("https://github.com/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: https://github.com/login/device and enter code: CA86-8D94
-
ブラウザーで https://github.com/login/device に移動し、前のステップのコードを入力して、 [続行] をクリックします。
-
GitHub で、アプリの承認を求めるページが表示されます。 [承認] ボタンをクリックします。
-
ターミナルに "Successfully authenticated!" と表示されます。
-
whoami
コマンドを追加する
アプリでユーザー アクセス トークンを生成できるようになったので、ユーザーに代わって API 要求を行うことができます。 認証されたユーザーのユーザー名を取得する whoami
コマンドを追加します。
-
次の
whoami
関数をapp_cli.rb
に追加します。 この関数は、/user
REST API エンドポイントでユーザーに関する情報を取得します。 ユーザー アクセス トークンに対応するユーザー名を出力します。.token
ファイルが見つからなかった場合は、login
関数を実行するようユーザーに求めます。Ruby def whoami uri = URI("https://api.github.com/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("https://api.github.com/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("https://github.com/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("https://github.com/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("https://api.github.com/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("https://github.com/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("https://github.com/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("https://api.github.com/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: https://github.com/login/device and enter code: CA86-8D94
-
ブラウザーで https://github.com/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 を作成するためのベスト プラクティス」をご覧ください。