소개
이 자습서에서는 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 Enterprise Server의 페이지 오른쪽 위 모서리에서 프로필 사진을 클릭합니다.
- 계정 설정으로 이동합니다.
- 개인 계정 소유한 앱의 경우 설정을 클릭합니다.
- 조직이 소유한 앱의 경우:
- 사용자의 조직을 클릭합니다.
- 조직 오른쪽에서 설정을 클릭합니다.
- 왼쪽 사이드바에서 개발자 설정을 클릭합니다.
- 왼쪽 사이드바에서 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("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이(가) 앱에 권한을 부여하라는 메시지가 있는 페이지를 표시합니다. "권한 부여" 단추를 클릭합니다.
-
이제 터미널에 "성공적으로 인증되었습니다!"라는 메시지가 표시됩니다.
-
터미널에서,
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을 만드는 모범 사례"을(를) 참조하세요.