소개
이 자습서에서는 GitHub App에서 지원되는 CLI(명령줄 인터페이스)를 빌드하는 방법과 디바이스 흐름을 사용하여 앱에 대한 사용자 액세스 토큰을 생성하는 방법을 보여 줍니다.
CLI에는 다음 세 가지 명령이 있습니다.
help
: 사용 지침을 출력합니다.login
: 앱이 사용자를 대신하여 API 요청을 만드는 데 사용할 수 있는 사용자 액세스 토큰을 생성합니다.whoami
: 로그인한 사용자에 대한 정보를 반환합니다.
이 자습서에서는 Ruby를 사용하지만 CLI를 작성하고 디바이스 흐름을 사용하여 프로그래밍 언어로 사용자 액세스 토큰을 생성할 수 있습니다.
디바이스 흐름 및 사용자 액세스 토큰 정보
CLI는 디바이스 흐름을 사용하여 사용자를 인증하고 사용자 액세스 토큰을 생성합니다. 그런 다음 CLI는 사용자 액세스 토큰을 사용하여 인증된 사용자를 대신하여 API 요청을 수행할 수 있습니다.
앱의 작업을 사용자에게 특성화하려면 앱에서 사용자 액세스 토큰을 사용해야 합니다. 자세한 내용은 "사용자를 대신하여 GitHub 앱 인증"을 참조하세요.
GitHub App에 대한 사용자 액세스 토큰을 생성하는 방법에는 웹 애플리케이션 흐름 및 디바이스 흐름의 두 가지가 있습니다. 앱이 헤드리스이거나 웹 인터페이스에 대한 액세스 권한이 없는 경우 디바이스 흐름을 사용하여 사용자 액세스 토큰을 생성해야 합니다. 예를 들어 CLI 도구, 간단한 Raspberry Pis 및 데스크톱 애플리케이션은 디바이스 흐름을 사용해야 합니다. 앱이 웹 인터페이스에 액세스할 수 있는 경우 웹 애플리케이션 흐름을 대신 사용해야 합니다. 자세한 내용은 "GitHub 앱 대한 사용자 액세스 토큰 생성" 및 "GitHub 앱 사용하여 "GitHub로 로그인" 단추 빌드.
필수 구성 요소
이 자습서에서는 GitHub App을(를) 이미 등록했다고 가정합니다. GitHub App를 등록하는 방법에 대한 자세한 내용은 "GitHub 앱 등록.
이 자습서를 따르기 전에 앱에 디바이스 흐름을 사용하도록 설정해야 합니다. 앱에 디바이스 흐름을 사용하도록 설정하는 방법에 대한 자세한 내용은 "GitHub 앱 등록 수정"을 참조하세요.
이 자습서에서는 Ruby에 대한 기본적인 이해가 있다고 가정합니다. 자세한 내용은 Ruby를 참조하세요.
클라이언트 ID 가져오기
디바이스 흐름을 통해 사용자 액세스 토큰을 생성하려면 앱의 클라이언트 ID가 필요합니다.
- 계정 설정으로 이동합니다.
- 개인 계정이 소유하는 GitHub App의 경우 페이지 오른쪽 위 모서리에서 프로필 사진을 클릭한 다음, 설정을 클릭합니다.
- 조직이 소유하는 GitHub App의 경우 페이지 오른쪽 위 모서리에서 프로필 사진을 클릭한 다음, 조직을 클릭합니다. 그런 다음, 조직 오른쪽에서 설정을 클릭합니다.
- 왼쪽 사이드바에서 개발자 설정을 클릭합니다.
- 왼쪽 사이드바에서 GitHub Apps를 클릭합니다.
- 작업할 GitHub App 옆에 있는 편집을 클릭합니다.
- 앱의 설정 페이지에서 앱의 클라이언트 ID를 찾습니다. 이 자습서의 뒷부분에서 사용합니다. 클라이언트 ID는 앱 ID와 다릅니다.
CLI 작성
이러한 단계는 CLI를 빌드하고 디바이스 흐름을 사용하여 사용자 액세스 토큰을 가져오는 방법을 안내합니다. 최종 코드로 건너뛰려면 "전체 코드 예제"를 참조하세요.
설치 프로그램
-
사용자 액세스 토큰을 생성하는 코드를 저장할 Ruby 파일을 만듭니다. 이 자습서에서는 파일
app_cli.rb
이름을 로 지정합니다. -
터미널의 가 저장된 디렉터리
app_cli.rb
에서 다음 명령을 실행하여 실행 파일을 만듭니app_cli.rb
다.Code chmod +x app_cli.rb
-
이 줄을 맨 위에
app_cli.rb
추가하여 Ruby 인터프리터를 사용하여 스크립트를 실행해야 함을 나타냅니다.Ruby #!/usr/bin/env ruby
-
다음
#!/usr/bin/env ruby
의app_cli.rb
맨 위에 이러한 종속성을 추가합니다.Ruby require "net/http" require "json" require "uri" require "fileutils"
모두 Ruby 표준 라이브러리의 일부이므로 보석을 설치할 필요가 없습니다.
-
진입점으로 사용할 다음
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
-
파일 맨 아래에 다음 줄을 추가하여 진입점 함수를 호출합니다. 자습서의 뒷부분에서 이 파일에 함수를 더 추가할 때 이 함수 호출은 파일 맨 아래에 남아 있어야 합니다.
Ruby 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
터미널의 가 저장된 디렉터리
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가 하나의 명령 "help"를 사용함을 알리는 줄을 인쇄합니다. 나중에 이help
함수를 확장합니다.Ruby def help puts "usage: app_cli <help>" end
-
명령이
main
제공되면 함수를help
호출하도록 함수를help
업데이트합니다.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
-
필요에 따라 진행률을 검사.
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
터미널의 가 저장된 디렉터리
app_cli.rb
에서 를 실행./app_cli.rb help
합니다. 다음 출력이 표시됩니다.usage: app_cli <help>
login
명령 추가
명령은 login
디바이스 흐름을 실행하여 사용자 액세스 토큰을 가져옵니다. 자세한 내용은 "GitHub 앱 대한 사용자 액세스 토큰 생성"을 참조하세요.
-
파일 맨 위에 있는 문 다음에
require
GitHub App의 를 에app_cli.rb
상수로 추가CLIENT_ID
합니다. 앱의 클라이언트 ID를 찾는 방법에 대한 자세한 내용은 "클라이언트 ID 가져오기"를 참조하세요. 을 앱의 클라이언트 ID로 바꿉니다YOUR_CLIENT_ID
.Ruby CLIENT_ID="YOUR_CLIENT_ID"
-
에 다음 함수를 추가합니다
parse_response
app_cli.rb
. 이 함수는 GitHub REST API의 응답을 구문 분석합니다. 응답 상태 또는201 Created
이200 OK
면 함수는 구문 분석된 응답 본문을 반환합니다. 그렇지 않으면 함수는 응답을 인쇄하고 본문은 프로그램을 종료합니다.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
-
에 다음 함수를 추가합니다
request_device_code
app_cli.rb
. 이 함수는 에POST
http(s)://HOSTNAME/login/device/code
요청을 수행하고 응답을 반환합니다.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
-
에 다음 함수를 추가합니다
request_token
app_cli.rb
. 이 함수는 에POST
http(s)://HOSTNAME/login/oauth/access_token
요청을 수행하고 응답을 반환합니다.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
-
에 다음 함수를 추가합니다
poll_for_token
app_cli.rb
. 이 함수는 GitHub이 매개 변수 대신 매개 변수로 응답할 때까지 지정된 간격으로access_token
error
폴링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
-
다음 함수를 추가합니다
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
- 함수를
-
명령이
main
제공되면 함수를login
호출하도록 함수를login
업데이트합니다.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
-
help
명령을 포함하도록 함수를 업데이트합니다.login
Ruby 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
-
터미널의 가 저장된 디렉터리
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
사용자의 사용자 이름을 가져오는 명령을 추가합니다.
-
에 다음
whoami
함수를 추가합니다app_cli.rb
. 이 함수는 REST API 엔드포인트를 사용하는 사용자에/user
대한 정보를 가져옵니다. 사용자 액세스 토큰에 해당하는 사용자 이름을 출력합니다..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
-
토큰이
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
-
명령이
main
제공되면 함수를whoami
호출하도록 함수를whoami
업데이트합니다.Ruby def main case ARGV[0] when "help" help when "login" login when "whoami" whoami else puts "Unknown command #{ARGV[0]}" end end
-
help
명령을 포함하도록 함수를 업데이트합니다.whoami
Ruby def help puts "usage: app_cli <login | whoami | help>" end
-
다음 섹션의 전체 코드 예제에 대해 코드를 확인합니다. 전체 코드 예제 아래의 "테스트" 섹션에 설명된 단계에 따라 코드를 테스트할 수 있습니다.
전체 코드 예제
이전 섹션에서 설명한 전체 코드 예제입니다. 을 앱의 클라이언트 ID로 바꿉니다 YOUR_CLIENT_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
테스트
이 자습서에서는 앱 코드가 라는 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 앱 만들기 위한 모범 사례"을 참조하세요.