소개
이 자습서에서는 GitHub App 기반의 CLI(명령줄 인터페이스)를 빌드하는 방법과 디바이스 흐름을 사용하여 앱에 대한 사용자 액세스 토큰을 생성하는 방법을 보여 줍니다.
이 CLI에는 다음 세 가지 명령이 포함됩니다.
help
: 사용 지침을 출력합니다.login
: 앱이 사용자를 대신하여 API 요청을 하는 데 사용할 수 있는 사용자 액세스 토큰을 생성합니다.whoami
: 로그인한 사용자에 대한 정보를 반환합니다.
이 자습서에서는 Ruby를 사용하지만, CLI를 작성하고 디바이스 흐름을 사용하여 프로그래밍 언어로 사용자 액세스 토큰을 생성할 수도 있습니다.
디바이스 흐름 및 사용자 액세스 토큰 정보
CLI는 디바이스 흐름을 사용하여 사용자를 인증하고 사용자 액세스 토큰을 생성합니다. 그런 다음 CLI는 사용자 액세스 토큰을 사용하여 인증된 사용자를 대신하여 API 요청을 수행할 수 있습니다.
앱의 작업을 사용자에게 귀속하려면 앱에서 사용자 액세스 토큰을 사용해야 합니다. 자세한 내용은 "사용자를 대신하여 GitHub 앱으로 인증"을 참조하세요.
GitHub App에 대한 사용자 액세스 토큰을 생성하는 방법에는 웹 애플리케이션 흐름과 디바이스 흐름의 두 가지가 있습니다. 앱이 비입력 시스템이거나 웹 인터페이스에 대한 액세스 권한이 없는 경우 디바이스 흐름을 사용하여 사용자 액세스 토큰을 생성해야 합니다. 예를 들어 CLI 도구, 간단한 라즈베리 파이 및 데스크톱 애플리케이션은 디바이스 흐름을 사용해야 합니다. 앱에 웹 인터페이스에 대한 액세스 권한이 있는 경우 웹 애플리케이션 흐름을 대신 사용해야 합니다. 자세한 내용은 "GitHub 앱에 대한 사용자 액세스 토큰 생성" 및 "GitHub 앱을 사용하여 "GitHub로 로그인" 단추 빌드"을(를) 참조하세요.
필수 조건
이 자습서에서는 GitHub App을(를) 이미 등록했다고 가정합니다. GitHub App 등록에 대한 자세한 내용은 "GitHub 앱 등록"을(를) 참조하세요.
이 자습서에 따라 진행하기 전에 먼저 앱에 대해 디바이스 흐름을 활성화해야 합니다. 앱에 대해 디바이스 흐름을 활성화하는 방법에 대한 자세한 내용은 "GitHub 앱 등록 수정"을(를) 참조하세요.
이 자습서에서는 Ruby에 대한 기본적인 이해를 갖추고 있다고 가정합니다. 자세한 내용은 Ruby를 참조하세요.
클라이언트 ID 가져오기
디바이스 흐름을 통해 사용자 액세스 토큰을 생성하려면 앱의 클라이언트 ID가 필요합니다.
- GitHub AE의 페이지 오른쪽 위 모서리에서 프로필 사진을 클릭합니다.
- 계정 설정으로 이동합니다.
- 개인 계정이 소유하는 GitHub App의 경우 설정을 클릭합니다.
- 조직이 소유한 GitHub App의 경우 다음을 수행합니다.
- 사용자의 조직을 클릭합니다.
- 조직 오른쪽에서 설정을 클릭합니다.
- 왼쪽 사이드바에서 개발자 설정을 클릭합니다.
- 왼쪽 사이드바에서 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
명령 추가
-
app_cli.rb
에 다음help
함수를 추가합니다. 현재help
함수는 사용자에게 이 CLI가 "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
문 뒤에 GitHub App의CLIENT_ID
를app_cli.rb
에 상수로 추가합니다. 앱의 클라이언트 ID를 찾는 방법에 대한 자세한 내용은 "클라이언트 ID 가져오기"를 참조하세요.YOUR_CLIENT_ID
를 앱의 클라이언트 ID로 바꿉니다.Ruby CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_ID="YOUR_CLIENT_ID"
-
app_cli.rb
에 다음parse_response
함수를 추가합니다. 이 함수는 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
-
app_cli.rb
에 다음request_device_code
함수를 추가합니다. 이 함수는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
-
app_cli.rb
에 다음request_token
함수를 추가합니다. 이 함수는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
-
app_cli.rb
에 다음poll_for_token
함수를 추가합니다. 이 함수는 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이(가) 앱에 권한을 부여하라는 메시지가 있는 페이지를 표시합니다. "권한 부여" 단추를 클릭합니다.
-
이제 터미널에 "성공적으로 인증되었습니다!"라는 메시지가 표시됩니다.
-
whoami
명령 추가
이제 앱이 사용자 액세스 토큰을 생성할 수 있으므로, 사용자를 대신해 API 요청을 수행할 수 있습니다. 인증된 사용자의 사용자 이름을 가져오는 whoami
명령을 추가합니다.
-
app_cli.rb
에 다음whoami
함수를 추가합니다. 이 함수는/user
REST API 엔드포인트를 사용하여 사용자에 대한 정보를 가져옵니다. 사용자 액세스 토큰에 해당하는 사용자 이름을 출력합니다..token
파일을 찾을 수 없으면 사용자에게login
함수를 실행하라는 메시지가 표시됩니다.Ruby def whoami uri = URI("https://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("https://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("https://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("https://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이(가) 앱에 권한을 부여하라는 메시지가 있는 페이지를 표시합니다. "권한 부여" 단추를 클릭합니다.
-
이제 터미널에 "성공적으로 인증되었습니다!"라는 메시지가 표시됩니다.
-
터미널에서,
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 앱 생성 모범 사례"을 참조하세요.