Introdução
Este tutorial demonstra como criar uma CLI (interface de linha de comando) apoiada por um GitHub App e como usar o fluxo do dispositivo para gerar um token de acesso do usuário para o aplicativo.
A CLI terá três comandos:
help
: gera as instruções de uso.login
: gera um token de acesso do usuário que o aplicativo pode usar para fazer solicitações de API em nome do usuário.whoami
: retorna informações sobre o usuário conectado.
Este tutorial usa o Ruby, mas você pode escrever uma CLI e usar o fluxo de dispositivo para gerar um token de acesso do usuário com qualquer linguagem de programação.
Sobre o fluxo do dispositivo e tokens de acesso do usuário
A CLI usará o fluxo do dispositivo para autenticar um usuário e gerar um token de acesso do usuário. Em seguida, a CLI pode usar o token de acesso do usuário para fazer solicitações de API em nome do usuário autenticado.
Seu aplicativo deve usar um token de acesso do usuário se você quiser atribuir as ações do aplicativo a um usuário. Para obter mais informações, confira "Autenticação com um aplicativo GitHub em nome de um usuário".
Há duas maneiras de gerar um token de acesso do usuário para um GitHub App: fluxo de aplicativo Web e fluxo de dispositivo. Você deve usar um fluxo de dispositivo para gerar um token de acesso do usuário se seu o aplicativo não tiver periféricos ou não tiver acesso a uma interface Web. Por exemplo, as ferramentas da CLI, o Raspberry Pis simples e os aplicativos da área de trabalho devem usar o fluxo do dispositivo. Se o aplicativo tiver acesso a uma interface da Web, você deverá usar o fluxo do aplicativo Web. Para obter mais informações, confira "Como gerar um token de acesso do usuário para um GitHub App" e "Criando um botão "Logon com o GitHub" com um Aplicativo GitHub."
Pré-requisitos
Este tutorial pressupõe que você já tenha registrado o GitHub App. Para obter mais informações sobre como registrar o GitHub App, confira "Registrar um Aplicativo GitHub".
Antes de seguir este tutorial, você deve habilitar o fluxo de dispositivo para seu aplicativo. Para obter mais informações sobre como habilitar o fluxo de dispositivos para seu aplicativo, confira "Modificar um registro do Aplicativo GitHub".
Este tutorial considera que você tenha uma compreensão básica do Ruby. Para obter mais informações, confira Ruby.
Obter a ID do cliente
Você precisará da ID do cliente do aplicativo para gerar um token de acesso do usuário por meio do fluxo do dispositivo.
- No canto superior direito de qualquer página do GitHub, clique na foto do seu perfil.
- Acesse as configurações da sua conta.
- Para um aplicativo de propriedade de uma conta pessoal, clique em Configurações.
- Para um aplicativo de propriedade de uma organização:
- Clique em Suas organizações.
- À direita da organização, clique em Configurações.
- Na barra lateral esquerda, clique em Configurações do desenvolvedor.
- Na barra lateral esquerda, clique em GitHub Apps .
- Ao lado do GitHub App com o qual deseja trabalhar, clique em Editar.
- Na página de configurações do aplicativo, encontre a ID do cliente para seu aplicativo. Você o usará posteriormente neste tutorial. Observe que a ID do cliente é diferente da ID do aplicativo.
Gravar a CLI
Essas etapas levam você à criação de uma CLI e ao uso do fluxo de dispositivo para obter um token de acesso do usuário. Para ir para o código final, confira "Exemplo de código completo".
Instalação
-
Crie um arquivo Ruby para manter o código que gerará um token de acesso do usuário. Este tutorial dará ao arquivo o nome
app_cli.rb
. -
No terminal, no diretório em que
app_cli.rb
está armazenado, execute o seguinte comando para tornarapp_cli.rb
executável:Text chmod +x app_cli.rb
chmod +x app_cli.rb
-
Adicione essa linha à parte superior de
app_cli.rb
para indicar que o interpretador do Ruby deve ser usado para executar o script:Ruby #!/usr/bin/env ruby
#!/usr/bin/env ruby
-
Adicione essas dependências à parte superior de
app_cli.rb
, depois de#!/usr/bin/env ruby
:Ruby require "net/http" require "json" require "uri" require "fileutils"
require "net/http" require "json" require "uri" require "fileutils"
Todos eles fazem parte da biblioteca padrão Ruby, portanto, você não precisa instalar nenhuma joia.
-
Adicione a função
main
a seguir que servirá como um ponto de entrada. A função inclui uma instruçãocase
para executar ações diferentes dependendo de qual comando é especificado. Você expandirá essa instruçãocase
mais tarde.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
-
Na parte inferior do arquivo, adicione a linha a seguir para chamar a função de ponto de entrada. Essa chamada de função deve permanecer na parte inferior do arquivo à medida que você adiciona mais funções a esse arquivo mais adiante no tutorial.
Ruby main
main
-
Opcionalmente, verifique seu progresso:
Agora
app_cli.rb
terá esta aparência: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
No terminal, no diretório em que
app_cli.rb
está armazenado, execute./app_cli.rb help
. Você deverá ver este resultado:`help` is not yet defined
Você também pode testar seu script sem um comando ou com um comando sem tratamento. Por exemplo,
./app_cli.rb create-issue
deve gerar:Unknown command `create-issue`
Adicionar um comando help
-
Adicionar a função
help
a seguir aapp_cli.rb
. Atualmente, a funçãohelp
imprime uma linha para informar aos usuários que essa CLI usa um comando, "ajuda". Você expandirá essa funçãohelp
mais tarde.Ruby def help puts "usage: app_cli <help>" end
def help puts "usage: app_cli <help>" end
-
Atualize a função
main
para chamar a funçãohelp
quando o comandohelp
for dado: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
-
Opcionalmente, verifique seu progresso:
Agora
app_cli.rb
terá esta aparência. A ordem das funções não importa, desde que a chamada de funçãomain
esteja no final do arquivo.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
No terminal, no diretório em que
app_cli.rb
está armazenado, execute./app_cli.rb help
. Você deverá ver este resultado:usage: app_cli <help>
Adicionar um comando login
O comando login
executará o fluxo do dispositivo para obter um token de acesso do usuário. Para obter mais informações, confira "Como gerar um token de acesso do usuário para um GitHub App".
-
Próximo à parte superior do arquivo, após as instruções
require
, adicione oCLIENT_ID
de seus GitHub App como uma constante emapp_cli.rb
. Para obter mais informações sobre como localizar a ID do cliente do aplicativo, confira "Obter a ID do cliente". SubstituaYOUR_CLIENT_ID
pela ID do cliente do aplicativo:Ruby CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_ID="YOUR_CLIENT_ID"
-
Adicionar a função
parse_response
a seguir aapp_cli.rb
. Essa função analisa uma resposta da API REST do GitHub. Quando a resposta status é200 OK
ou201 Created
, a função retorna o corpo da resposta analisada. Caso contrário, a função imprime a resposta e o corpo e sai do programa.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
-
Adicionar a função
request_device_code
a seguir aapp_cli.rb
. Essa função faz uma solicitaçãoPOST
parahttp(s)://HOSTNAME/login/device/code
e retorna a resposta.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
-
Adicionar a função
request_token
a seguir aapp_cli.rb
. Essa função faz uma solicitaçãoPOST
parahttp(s)://HOSTNAME/login/oauth/access_token
e retorna a resposta.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
-
Adicionar a função
poll_for_token
a seguir aapp_cli.rb
. Essa função sondahttp(s)://HOSTNAME/login/oauth/access_token
no intervalo especificado até que GitHub responda com um parâmetroaccess_token
em vez de um parâmetroerror
. Em seguida, ele grava o token de acesso do usuário em um arquivo e restringe as permissões no arquivo.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
-
Adicionar a função
login
a seguir.Esta função:
- Chama a função
request_device_code
e obtém os parâmetrosverification_uri
,user_code``device_code
einterval
da resposta. - Solicita que os usuários insiram o
user_code
da etapa anterior. - Chama o
poll_for_token
para sondar GitHub para obter um token de acesso. - Informa ao usuário que a autenticação foi bem-sucedida.
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
- Chama a função
-
Atualize a função
main
para chamar a funçãologin
quando o comandologin
for dado: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
-
Atualize a função
help
para incluir o comandologin
:Ruby def help puts "usage: app_cli <login | help>" end
def help puts "usage: app_cli <login | help>" end
-
Opcionalmente, verifique seu progresso:
app_cli.rb
agora se parece com isso, em queYOUR_CLIENT_ID
é a ID do cliente do seu aplicativo. A ordem das funções não importa, desde que a chamada de funçãomain
esteja no final do arquivo.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
-
No terminal, no diretório em que
app_cli.rb
está armazenado, execute./app_cli.rb login
. Você deverá ver uma saída semelhante a esta. O código será diferente sempre que:Please visit: http(s)://HOSTNAME/login/device and enter code: CA86-8D94
-
Navegue até http(s)://HOSTNAME/login/device no navegador e insira o código da etapa anterior e clique em Continuar.
-
GitHub deve exibir uma página que solicita que você autorize seu aplicativo. Clique no botão "Autorizar".
-
Seu terminal agora deve dizer "Autenticado com êxito!".
-
Adicionar um comando whoami
Agora que seu aplicativo pode gerar um token de acesso do usuário, você pode fazer solicitações de API em nome do usuário. Adicione um comando whoami
para obter o nome de usuário do usuário autenticado.
-
Adicionar a função
whoami
a seguir aapp_cli.rb
. Esta função obtém informações sobre o usuário com o ponto de extremidade/user
da API REST. Ele gera o nome de usuário que corresponde ao token de acesso do usuário. Se o arquivo.token
não tiver sido encontrado, ele solicitará que o usuário execute a funçãologin
.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
-
Atualize a função
parse_response
para lidar com o caso em que o token expirou ou foi revogado. Agora, se você receber uma resposta401 Unauthorized
, a CLI solicitará que o usuário execute o comandologin
.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
-
Atualize a função
main
para chamar a funçãowhoami
quando o comandowhoami
for dado: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
-
Atualize a função
help
para incluir o comandowhoami
:Ruby def help puts "usage: app_cli <login | whoami | help>" end
def help puts "usage: app_cli <login | whoami | help>" end
-
Verifique o código em relação ao exemplo de código completo na próxima seção. Teste o código seguindo as etapas descritas na seção "Teste" abaixo do exemplo de código completo.
Exemplo de código completo
Este é o exemplo de código completo descrito na seção anterior. Substitua YOUR_CLIENT_ID
pela ID do cliente do aplicativo.
#!/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
Testando
Este tutorial pressupõe que o código do aplicativo esteja armazenado em um arquivo chamado app_cli.rb
.
-
No terminal, no diretório em que
app_cli.rb
está armazenado, execute./app_cli.rb help
. Você deverá ver uma saída semelhante a esta.usage: app_cli <login | whoami | help>
-
No terminal, no diretório em que
app_cli.rb
está armazenado, execute./app_cli.rb login
. Você deverá ver uma saída semelhante a esta. O código será diferente sempre que:Please visit: http(s)://HOSTNAME/login/device and enter code: CA86-8D94
-
Navegue até http(s)://HOSTNAME/login/device no navegador e insira o código da etapa anterior e clique em Continuar.
-
GitHub deve exibir uma página que solicita que você autorize seu aplicativo. Clique no botão "Autorizar".
-
Seu terminal agora deve dizer "Autenticado com êxito!".
-
No terminal, no diretório em que
app_cli.rb
está armazenado, execute./app_cli.rb whoami
. Você deverá ver uma saída semelhante a esta, em queoctocat
é seu nome de usuário.You are octocat
-
Abra o arquivo
.token
no editor e modifique o token. Agora, o token é inválido. -
No terminal, no diretório em que
app_cli.rb
está armazenado, execute./app_cli.rb whoami
. Você deverá ver uma saída parecida com esta:You are not authorized. Run the `login` command.
-
Exclua o arquivo
.token
. -
No terminal, no diretório em que
app_cli.rb
está armazenado, execute./app_cli.rb whoami
. Você deverá ver uma saída semelhante a esta:You are not authorized. Run the `login` command.
Próximas etapas
Ajustar o código de acordo com as necessidades do aplicativo
Este tutorial demonstrou como escrever uma CLI que usa o fluxo do dispositivo para gerar um token de acesso do usuário. Você pode expandir essa CLI para aceitar comandos adicionais. Por exemplo, você pode adicionar um comando create-issue
que abre um problema. Lembre-se de atualizar as permissões do aplicativo se o aplicativo precisar de permissões adicionais para as solicitações de API que você deseja fazer. Para obter mais informações, confira "Escolhendo permissões para um Aplicativo GitHub".
Armazenar tokens com segurança
Este tutorial gera um token de acesso do usuário e o salva em um arquivo local. Você nunca deve confirmar esse arquivo ou divulgar o token.
Dependendo do seu dispositivo, você pode escolher diferentes maneiras de armazenar o token. Você deve marcar as práticas recomendadas para armazenar tokens em seu dispositivo.
Para obter mais informações, confira "Práticas recomendadas para criar um aplicativo do GitHub".
Seguir as práticas recomendadas
Você deve ter como objetivo seguir as melhores práticas com seu GitHub App. Para obter mais informações, confira "Práticas recomendadas para criar um aplicativo do GitHub".