Skip to main content

Creación de una CLI con una aplicación de GitHub

Sigue este tutorial para escribir una CLI en Ruby que genere un token de acceso de usuario para una GitHub App a través del flujo de dispositivo.

Introducción

En este tutorial se muestra cómo crear una interfaz de línea de comandos (CLI) respaldada por una GitHub App y cómo usar el flujo de dispositivo para generar un token de acceso de usuario para la aplicación.

La CLI tendrá tres comandos:

  • help: genera las instrucciones de uso.
  • login: genera un token de acceso de usuario que la aplicación puede usar para realizar solicitudes de API en nombre del usuario.
  • whoami: devuelve información sobre el usuario que ha iniciado sesión.

En este tutorial se usa Ruby, pero puedes escribir una CLI y usar el flujo de dispositivo para generar un token de acceso de usuario con cualquier lenguaje de programación.

Acerca del flujo de dispositivo y los tokens de acceso de usuario

La CLI usará el flujo de dispositivo para autenticar a un usuario y generar un token de acceso de usuario. A continuación, la CLI puede usar el token de acceso de usuario para realizar solicitudes de API en nombre del usuario autenticado.

La aplicación debe usar un token de acceso de usuario si quieres atribuir las acciones de la aplicación a un usuario. Para obtener más información, vea «Autenticación con una aplicación de GitHub en nombre de un usuario».

Hay dos maneras de generar un token de acceso de usuario para la GitHub App: flujo de aplicación web y flujo de dispositivo. Debes usar el flujo de dispositivo para generar un token de acceso de usuario si la aplicación no tiene acceso a una interfaz web o está desatendida. Por ejemplo, las herramientas de la CLI, Raspberry Pis simples y las aplicaciones de escritorio deben usar el flujo de dispositivo. Si la aplicación tiene acceso a una interfaz web, debes usar el flujo de aplicaciones web en su lugar. Para obtener más información, vea «Generación de un token de acceso de usuario para una aplicación de GitHub» y «Creación de un botón "Inicio de sesión con GitHub" con una aplicación de GitHub».

Requisitos previos

En este tutorial se supone que ya has registrado una GitHub App. Para obtener más información sobre el registro de una GitHub App, consulta "Registro de una instancia de GitHub App".

Antes de seguir este tutorial, debes habilitar el flujo de dispositivos para la aplicación. Para más información sobre cómo habilitar el flujo de dispositivo para la aplicación, consulta "Modificación del registro de una instancia de GitHub App".

En este tutorial se da por hecho que posees un conocimiento básico de Ruby. Para más información, consulta Ruby.

Obtención del identificador de cliente

Necesitarás el identificador de cliente de la aplicación para generar un token de acceso de usuario a través del flujo de dispositivo.

  1. En la esquina superior derecha de cualquier página en GitHub, haga clic en su fotografía de perfil.
  2. Navega a la configuración de tu cuenta.
    • Para una aplicación propiedad de una cuenta personal, haga clic en Configuración.
    • Para una aplicación propiedad de una organización:
      1. Haga clic en Sus organizaciones.
      2. A la derecha de la organización, haga clic en Configuración.
  3. En la barra lateral izquierda, haz clic en Configuración del desarrollador.
  4. En la barra lateral de la izquierda, haga clic en GitHub Apps .
  5. Junto a la GitHub App que quieres modificar, haz clic en Editar.
  6. En la página de configuración de la aplicación, busca el identificador de cliente de la aplicación. Lo usarás más adelante en este tutorial. El identificador de cliente es diferente del identificador de la aplicación.

Escritura de la CLI

Estos pasos te llevan a crear una CLI y a usar el flujo de dispositivo para obtener un token de acceso de usuario. Para ir directamente al código final, consulta "Ejemplo de código completo".

Configurar

  1. Crea un archivo de Ruby para contener el código que generará un token de acceso de usuario. En este tutorial se llamará al archivo app_cli.rb.

  2. En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta el siguiente comando para convertir a app_cli.rb en ejecutable:

    Text
    chmod +x app_cli.rb
    
  3. Agrega esta línea a la parte superior de app_cli.rb para indicar que se debe usar el intérprete de Ruby para ejecutar el script:

    Ruby
    #!/usr/bin/env ruby
    
  4. Agrega estas dependencias a la parte superior de app_cli.rb, a continuación de #!/usr/bin/env ruby:

    Ruby
    require "net/http"
    require "json"
    require "uri"
    require "fileutils"
    

    Todas forman parte de la biblioteca estándar de Ruby, por lo que no es necesario instalar ninguna gema.

  5. Agrega la siguiente función main que servirá como punto de entrada. La función incluye una instrucción case para realizar diferentes acciones en función del comando especificado. Expandirás esta instrucción case más adelante.

    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. En la parte inferior del archivo, agrega la siguiente línea para llamar a la función de punto de entrada. Esta llamada a función debe permanecer en la parte inferior del archivo a medida que agregues más funciones a este archivo más adelante en el tutorial.

    Ruby
    main
    
  7. Opcionalmente, comprueba el progreso:

    app_cli.rb ahora tiene este aspecto.

    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
    

    En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta ./app_cli.rb help. Debería ver este resultado:

    `help` is not yet defined
    

    También puedes probar el script sin un comando o con un comando no controlado. Por ejemplo, ./app_cli.rb create-issue debe generar:

    Unknown command `create-issue`
    

Adición de un comando help

  1. Agrega la siguiente función help a app_cli.rb: Actualmente, la función help imprime una línea para indicar a los usuarios que esta CLI toma un comando, "help". Expandirás esta función help más adelante.

    Ruby
    def help
      puts "usage: app_cli <help>"
    end
    
  2. Actualiza la función main para llamar a la función help cuando se especifique el comando 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
    
  3. Opcionalmente, comprueba el progreso:

    app_cli.rb ahora tiene este aspecto. El orden de las funciones no importa siempre que la llamada de función main esté al final del archivo.

    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
    

    En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta ./app_cli.rb help. Debería ver este resultado:

    usage: app_cli <help>
    

Adición de un comando login

El comando login ejecutará el flujo de dispositivo para obtener un token de acceso de usuario. Para obtener más información, vea «Generación de un token de acceso de usuario para una aplicación de GitHub».

  1. Cerca de la parte superior del archivo, después de las instrucciones require, agrega el valor CLIENT_ID de la GitHub App como una constante en app_cli.rb. Para más información sobre cómo buscar el identificador de cliente de la aplicación, consulta "Obtención del identificador de cliente". Reemplaza YOUR_CLIENT_ID por el identificador de cliente de la aplicación:

    Ruby
    CLIENT_ID="YOUR_CLIENT_ID"
    
  2. Agrega la siguiente función parse_response a app_cli.rb: Esta función analiza una respuesta de la API REST de GitHub. Cuando el estado de la respuesta es 200 OK o 201 Created, la función devuelve el cuerpo de la respuesta analizada. De lo contrario, la función imprime la respuesta y el cuerpo y sale del 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. Agrega la siguiente función request_device_code a app_cli.rb: Esta función realiza una solicitud POST a https://github.com/login/device/code y devuelve la respuesta.

    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. Agrega la siguiente función request_token a app_cli.rb: Esta función realiza una solicitud POST a https://github.com/login/oauth/access_token y devuelve la respuesta.

    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. Agrega la siguiente función poll_for_token a app_cli.rb: Esta función sondea https://github.com/login/oauth/access_token en el intervalo especificado hasta que la GitHub responde con un parámetro access_token en lugar de un parámetro error. A continuación, escribe el token de acceso de usuario en un archivo y restringe los permisos en el archivo.

    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. Agrega la siguiente función login:

    Esta función:

    1. Llama a la función request_device_code y obtiene los parámetros verification_uri, user_code, device_code y interval de la respuesta.
    2. Solicita a los usuarios que introduzcan el user_code del paso anterior.
    3. Llama a poll_for_token para sondear GitHub para un token de acceso.
    4. Permite al usuario saber que la autenticación se realizó correctamente.
    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. Actualiza la función main para llamar a la función login cuando se especifique el comando 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
    
  8. Actualiza la función help para incluir el comando login:

    Ruby
    def help
      puts "usage: app_cli <login | help>"
    end
    
  9. Opcionalmente, comprueba el progreso:

    app_cli.rb ahora tiene un aspecto similar al siguiente, donde YOUR_CLIENT_ID es el identificador de cliente de la aplicación. El orden de las funciones no importa siempre que la llamada de función main esté al final del archivo.

    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. En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta ./app_cli.rb login. Debería aparecer un resultado como el siguiente. El código variará cada vez:

      Please visit: https://github.com/login/device
      and enter code: CA86-8D94
      
    2. Ve a https://github.com/login/device en el explorador e introduce el código del paso anterior y, a continuación, haz clic en Continuar.

    3. GitHub debe mostrar una página que te pida que autorices la aplicación. Haz clic en el botón "Autorizar".

    4. El terminal debería decir ahora "Autenticado correctamente".

Adición de un comando whoami

Ahora que la aplicación puede generar un token de acceso de usuario, puedes realizar solicitudes de API en nombre del usuario. Agrega un comando whoami para obtener el nombre de usuario del usuario autenticado.

  1. Agrega la siguiente función whoami a app_cli.rb: Esta función obtiene información sobre el usuario con el punto de conexión de la API REST /user. Genera el nombre de usuario que corresponde al token de acceso de usuario. Si no se encontró el archivo .token, solicita al usuario que ejecute la función 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. Actualiza la función parse_response para controlar el caso en el que el token ha expirado o se ha revocado. Ahora, si recibes una respuesta 401 Unauthorized, la CLI pedirá al usuario que ejecute el 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. Actualiza la función main para llamar a la función whoami cuando se especifique el comando whoami:

    Ruby
    def main
      case ARGV[0]
      when "help"
        help
      when "login"
        login
      when "whoami"
        whoami
      else
        puts "Unknown command #{ARGV[0]}"
      end
    end
    
  4. Actualiza la función help para incluir el comando whoami:

    Ruby
    def help
      puts "usage: app_cli <login | whoami | help>"
    end
    
  5. Comprueba el código en el ejemplo de código completo de la sección siguiente. Puedes probar el código siguiendo los pasos descritos en la sección "Pruebas" debajo del ejemplo de código completo.

Ejemplo de código completo

Este es el ejemplo de código completo que se describió en la sección anterior. Reemplaza YOUR_CLIENT_ID por el identificador de cliente por de la aplicación.

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

Prueba

En este tutorial se supone que el código de la aplicación se almacena en un archivo denominado app_cli.rb.

  1. En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta ./app_cli.rb help. Debería aparecer un resultado como el siguiente.

    usage: app_cli <login | whoami | help>
    
  2. En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta ./app_cli.rb login. Debería aparecer un resultado como el siguiente. El código variará cada vez:

    Please visit: https://github.com/login/device
    and enter code: CA86-8D94
    
  3. Ve a https://github.com/login/device en el explorador e introduce el código del paso anterior y, a continuación, haz clic en Continuar.

  4. GitHub debe mostrar una página que te pida que autorices la aplicación. Haz clic en el botón "Autorizar".

  5. El terminal debería decir ahora "Autenticado correctamente".

  6. En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta ./app_cli.rb whoami. Deberías ver la salida que tiene este aspecto, donde octocat es el nombre de usuario.

    You are octocat
    
  7. Abre el archivo en el editor .token y modifica el token. Ahora, el token no es válido.

  8. En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta ./app_cli.rb whoami. Debería aparecer un resultado como el siguiente:

    You are not authorized. Run the `login` command.
    
  9. Elimine el archivo .token.

  10. En el terminal, desde el directorio donde se almacena app_cli.rb, ejecuta ./app_cli.rb whoami. Deberías obtener un resultado similar al siguiente:

    You are not authorized. Run the `login` command.
    

Pasos siguientes

Ajuste del código para satisfacer las necesidades de la aplicación

En este tutorial se muestra cómo escribir una CLI que usa el flujo de dispositivo para generar un token de acceso de usuario. Puedes expandir esta CLI para aceptar comandos adicionales. Por ejemplo, puedes agregar un comando create-issue que abra un problema. Recuerda actualizar los permisos de la aplicación si esta necesita permisos adicionales para las solicitudes de API que quieres realizar. Para obtener más información, vea «Elección de permisos para una aplicación de GitHub».

Almacenamiento seguro de tokens

Este tutorial genera un token de acceso de usuario y lo guarda en un archivo local. Nunca debes confirmar este archivo ni hacer público el token.

En función del dispositivo, puede elegir maneras diferentes de almacenar el token. Debes comprobar los procedimientos recomendados para almacenar tokens en el dispositivo.

Para obtener más información, vea «Procedimientos recomendados para crear una aplicación de GitHub».

Seguimiento de los procedimientos recomendados

Debes intentar seguir los procedimientos recomendados con tu instancia de GitHub App. Para obtener más información, vea «Procedimientos recomendados para crear una aplicación de GitHub».