Skip to main content

Criando uma CLI com um Aplicativo GitHub

Siga este tutorial para escrever uma CLI no Ruby que gera um token de acesso do usuário para um GitHub App por meio do fluxo do dispositivo.

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.

  1. No canto superior direito de qualquer página do GitHub, clique na foto do seu perfil.
  2. 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:
      1. Clique em Suas organizações.
      2. À direita da organização, clique em Configurações.
  3. Na barra lateral esquerda, clique em Configurações do desenvolvedor.
  4. Na barra lateral esquerda, clique em GitHub Apps .
  5. Ao lado do GitHub App com o qual deseja trabalhar, clique em Editar.
  6. 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

  1. 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.

  2. No terminal, no diretório em que app_cli.rb está armazenado, execute o seguinte comando para tornar app_cli.rb executável:

    Text
    chmod +x app_cli.rb
    
  3. 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
    
  4. 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"
    

    Todos eles fazem parte da biblioteca padrão Ruby, portanto, você não precisa instalar nenhuma joia.

  5. Adicione a função main a seguir que servirá como um ponto de entrada. A função inclui uma instrução case para executar ações diferentes dependendo de qual comando é especificado. Você expandirá essa instrução case 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
    
  6. 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
    
  7. 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
    

    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

  1. Adicionar a função help a seguir a app_cli.rb. Atualmente, a função help imprime uma linha para informar aos usuários que essa CLI usa um comando, "ajuda". Você expandirá essa função help mais tarde.

    Ruby
    def help
      puts "usage: app_cli <help>"
    end
    
  2. Atualize a função main para chamar a função help quando o comando help 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
    
  3. 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ção main 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
    

    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".

  1. Próximo à parte superior do arquivo, após as instruções require, adicione o CLIENT_ID de seus GitHub App como uma constante em app_cli.rb. Para obter mais informações sobre como localizar a ID do cliente do aplicativo, confira "Obter a ID do cliente". Substitua YOUR_CLIENT_ID pela ID do cliente do aplicativo:

    Ruby
    CLIENT_ID="YOUR_CLIENT_ID"
    
  2. Adicionar a função parse_response a seguir a app_cli.rb. Essa função analisa uma resposta da API REST do GitHub. Quando a resposta status é 200 OK ou 201 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
    
  3. Adicionar a função request_device_code a seguir a app_cli.rb. Essa função faz uma solicitação POST para https://github.com/login/device/code e retorna a resposta.

    Ruby
    def request_device_code
      uri = URI("https://github.com/login/device/code")
      parameters = URI.encode_www_form("client_id" => CLIENT_ID)
      headers = {"Accept" => "application/json"}
    
      response = Net::HTTP.post(uri, parameters, headers)
      parse_response(response)
    end
    
  4. Adicionar a função request_token a seguir a app_cli.rb. Essa função faz uma solicitação POST para https://github.com/login/oauth/access_token e retorna a resposta.

    Ruby
    def request_token(device_code)
      uri = URI("https://github.com/login/oauth/access_token")
      parameters = URI.encode_www_form({
        "client_id" => CLIENT_ID,
        "device_code" => device_code,
        "grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
      })
      headers = {"Accept" => "application/json"}
      response = Net::HTTP.post(uri, parameters, headers)
      parse_response(response)
    end
    
  5. Adicionar a função poll_for_token a seguir a app_cli.rb. Essa função sonda https://github.com/login/oauth/access_token no intervalo especificado até que GitHub responda com um parâmetro access_token em vez de um parâmetro error. 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
    
  6. Adicionar a função login a seguir.

    Esta função:

    1. Chama a função request_device_code e obtém os parâmetros verification_uri, user_code``device_code e interval da resposta.
    2. Solicita que os usuários insiram o user_code da etapa anterior.
    3. Chama o poll_for_token para sondar GitHub para obter um token de acesso.
    4. 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
    
  7. Atualize a função main para chamar a função login quando o comando login 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
    
  8. Atualize a função help para incluir o comando login:

    Ruby
    def help
      puts "usage: app_cli <login | help>"
    end
    
  9. Opcionalmente, verifique seu progresso:

    app_cli.rb agora se parece com isso, em que YOUR_CLIENT_ID é a ID do cliente do seu aplicativo. A ordem das funções não importa, desde que a chamada de função main 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("https://github.com/login/device/code")
      parameters = URI.encode_www_form("client_id" => CLIENT_ID)
      headers = {"Accept" => "application/json"}
    
      response = Net::HTTP.post(uri, parameters, headers)
      parse_response(response)
    end
    
    def request_token(device_code)
      uri = URI("https://github.com/login/oauth/access_token")
      parameters = URI.encode_www_form({
        "client_id" => CLIENT_ID,
        "device_code" => device_code,
        "grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
      })
      headers = {"Accept" => "application/json"}
      response = Net::HTTP.post(uri, parameters, headers)
      parse_response(response)
    end
    
    def poll_for_token(device_code, interval)
    
      loop do
        response = request_token(device_code)
        error, access_token = response.values_at("error", "access_token")
    
        if error
          case error
          when "authorization_pending"
            # The user has not yet entered the code.
            # Wait, then poll again.
            sleep interval
            next
          when "slow_down"
            # The app polled too fast.
            # Wait for the interval plus 5 seconds, then poll again.
            sleep interval + 5
            next
          when "expired_token"
            # The `device_code` expired, and the process needs to restart.
            puts "The device code has expired. Please run `login` again."
            exit 1
          when "access_denied"
            # The user cancelled the process. Stop polling.
            puts "Login cancelled by user."
            exit 1
          else
            puts response
            exit 1
          end
        end
    
        File.write("./.token", access_token)
    
        # Set the file permissions so that only the file owner can read or modify the file
        FileUtils.chmod(0600, "./.token")
    
        break
      end
    end
    
    def login
      verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval")
    
      puts "Please visit: #{verification_uri}"
      puts "and enter code: #{user_code}"
    
      poll_for_token(device_code, interval)
    
      puts "Successfully authenticated!"
    end
    
    main
    
    1. 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: https://github.com/login/device
      and enter code: CA86-8D94
      
    2. Navegue até https://github.com/login/device no navegador e insira o código da etapa anterior e clique em Continuar.

    3. GitHub deve exibir uma página que solicita que você autorize seu aplicativo. Clique no botão "Autorizar".

    4. 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.

  1. Adicionar a função whoami a seguir a app_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ção login.

    Ruby
    def whoami
      uri = URI("https://api.github.com/user")
    
      begin
        token = File.read("./.token").strip
      rescue Errno::ENOENT => e
        puts "You are not authorized. Run the `login` command."
        exit 1
      end
    
      response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
        body = {"access_token" => token}.to_json
        headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"}
    
        http.send_request("GET", uri.path, body, headers)
      end
    
      parsed_response = parse_response(response)
      puts "You are #{parsed_response["login"]}"
    end
    
  2. Atualize a função parse_response para lidar com o caso em que o token expirou ou foi revogado. Agora, se você receber uma resposta 401 Unauthorized, a CLI solicitará que o usuário execute o comando 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
    
  3. Atualize a função main para chamar a função whoami quando o comando whoami 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
    
  4. Atualize a função help para incluir o comando whoami:

    Ruby
    def help
      puts "usage: app_cli <login | whoami | help>"
    end
    
  5. 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.

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 | whoami | help>"
end

def main
  case ARGV[0]
  when "help"
    help
  when "login"
    login
  when "whoami"
    whoami
  else
    puts "Unknown command #{ARGV[0]}"
  end
end

def parse_response(response)
  case response
  when Net::HTTPOK, Net::HTTPCreated
    JSON.parse(response.body)
  when Net::HTTPUnauthorized
    puts "You are not authorized. Run the `login` command."
    exit 1
  else
    puts response
    puts response.body
    exit 1
  end
end

def request_device_code
  uri = URI("https://github.com/login/device/code")
  parameters = URI.encode_www_form("client_id" => CLIENT_ID)
  headers = {"Accept" => "application/json"}

  response = Net::HTTP.post(uri, parameters, headers)
  parse_response(response)
end

def request_token(device_code)
  uri = URI("https://github.com/login/oauth/access_token")
  parameters = URI.encode_www_form({
    "client_id" => CLIENT_ID,
    "device_code" => device_code,
    "grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
  })
  headers = {"Accept" => "application/json"}
  response = Net::HTTP.post(uri, parameters, headers)
  parse_response(response)
end

def poll_for_token(device_code, interval)

  loop do
    response = request_token(device_code)
    error, access_token = response.values_at("error", "access_token")

    if error
      case error
      when "authorization_pending"
        # The user has not yet entered the code.
        # Wait, then poll again.
        sleep interval
        next
      when "slow_down"
        # The app polled too fast.
        # Wait for the interval plus 5 seconds, then poll again.
        sleep interval + 5
        next
      when "expired_token"
        # The `device_code` expired, and the process needs to restart.
        puts "The device code has expired. Please run `login` again."
        exit 1
      when "access_denied"
        # The user cancelled the process. Stop polling.
        puts "Login cancelled by user."
        exit 1
      else
        puts response
        exit 1
      end
    end

    File.write("./.token", access_token)

    # Set the file permissions so that only the file owner can read or modify the file
    FileUtils.chmod(0600, "./.token")

    break
  end
end

def login
  verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval")

  puts "Please visit: #{verification_uri}"
  puts "and enter code: #{user_code}"

  poll_for_token(device_code, interval)

  puts "Successfully authenticated!"
end

def whoami
  uri = URI("https://api.github.com/user")

  begin
    token = File.read("./.token").strip
  rescue Errno::ENOENT => e
    puts "You are not authorized. Run the `login` command."
    exit 1
  end

  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    body = {"access_token" => token}.to_json
    headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"}

    http.send_request("GET", uri.path, body, headers)
  end

  parsed_response = parse_response(response)
  puts "You are #{parsed_response["login"]}"
end

main

Testando

Este tutorial pressupõe que o código do aplicativo esteja armazenado em um arquivo chamado app_cli.rb.

  1. 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>
    
  2. 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: https://github.com/login/device
    and enter code: CA86-8D94
    
  3. Navegue até https://github.com/login/device no navegador e insira o código da etapa anterior e clique em Continuar.

  4. GitHub deve exibir uma página que solicita que você autorize seu aplicativo. Clique no botão "Autorizar".

  5. Seu terminal agora deve dizer "Autenticado com êxito!".

  6. 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 que octocat é seu nome de usuário.

    You are octocat
    
  7. Abra o arquivo .token no editor e modifique o token. Agora, o token é inválido.

  8. 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.
    
  9. Exclua o arquivo .token.

  10. 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".