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.
- En la esquina superior derecha de cualquier página en GitHub, haga clic en su fotografía de perfil.
- 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:
- Haga clic en Sus organizaciones.
- A la derecha de la organización, haga clic en Configuración.
- En la barra lateral izquierda, haz clic en Configuración del desarrollador.
- En la barra lateral de la izquierda, haga clic en GitHub Apps .
- Junto a la GitHub App que quieres modificar, haz clic en Editar.
- 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
-
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
. -
En el terminal, desde el directorio donde se almacena
app_cli.rb
, ejecuta el siguiente comando para convertir aapp_cli.rb
en ejecutable:Text chmod +x app_cli.rb
chmod +x app_cli.rb
-
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
#!/usr/bin/env ruby
-
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"
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.
-
Agrega la siguiente función
main
que servirá como punto de entrada. La función incluye una instruccióncase
para realizar diferentes acciones en función del comando especificado. Expandirás esta instruccióncase
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
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
-
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
main
-
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
#!/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
-
Agrega la siguiente función
help
aapp_cli.rb
: Actualmente, la funciónhelp
imprime una línea para indicar a los usuarios que esta CLI toma un comando, "help". Expandirás esta funciónhelp
más adelante.Ruby def help puts "usage: app_cli <help>" end
def help puts "usage: app_cli <help>" end
-
Actualiza la función
main
para llamar a la funciónhelp
cuando se especifique el comandohelp
: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, comprueba el progreso:
app_cli.rb
ahora tiene este aspecto. El orden de las funciones no importa siempre que la llamada de funciónmain
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
#!/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».
-
Cerca de la parte superior del archivo, después de las instrucciones
require
, agrega el valorCLIENT_ID
de la GitHub App como una constante enapp_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". ReemplazaYOUR_CLIENT_ID
por el identificador de cliente de la aplicación:Ruby CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_ID="YOUR_CLIENT_ID"
-
Agrega la siguiente función
parse_response
aapp_cli.rb
: Esta función analiza una respuesta de la API REST de GitHub. Cuando el estado de la respuesta es200 OK
o201 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
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
-
Agrega la siguiente función
request_device_code
aapp_cli.rb
: Esta función realiza una solicitudPOST
ahttps://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
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
-
Agrega la siguiente función
request_token
aapp_cli.rb
: Esta función realiza una solicitudPOST
ahttps://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
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
-
Agrega la siguiente función
poll_for_token
aapp_cli.rb
: Esta función sondeahttps://github.com/login/oauth/access_token
en el intervalo especificado hasta que la GitHub responde con un parámetroaccess_token
en lugar de un parámetroerror
. 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
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
-
Agrega la siguiente función
login
:Esta función:
- Llama a la función
request_device_code
y obtiene los parámetrosverification_uri
,user_code
,device_code
yinterval
de la respuesta. - Solicita a los usuarios que introduzcan el
user_code
del paso anterior. - Llama a
poll_for_token
para sondear GitHub para un token de acceso. - 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
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
- Llama a la función
-
Actualiza la función
main
para llamar a la funciónlogin
cuando se especifique el comandologin
: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
-
Actualiza la función
help
para incluir el comandologin
:Ruby def help puts "usage: app_cli <login | help>" end
def help puts "usage: app_cli <login | help>" end
-
Opcionalmente, comprueba el progreso:
app_cli.rb
ahora tiene un aspecto similar al siguiente, dondeYOUR_CLIENT_ID
es el identificador de cliente de la aplicación. El orden de las funciones no importa siempre que la llamada de funciónmain
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
#!/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
-
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
-
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.
-
GitHub debe mostrar una página que te pida que autorices la aplicación. Haz clic en el botón "Autorizar".
-
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.
-
Agrega la siguiente función
whoami
aapp_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ónlogin
.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
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
-
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 respuesta401 Unauthorized
, la CLI pedirá al usuario que ejecute el 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
-
Actualiza la función
main
para llamar a la funciónwhoami
cuando se especifique el comandowhoami
: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
-
Actualiza la función
help
para incluir el comandowhoami
:Ruby def help puts "usage: app_cli <login | whoami | help>" end
def help puts "usage: app_cli <login | whoami | help>" end
-
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.
#!/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
#!/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
.
-
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>
-
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
-
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.
-
GitHub debe mostrar una página que te pida que autorices la aplicación. Haz clic en el botón "Autorizar".
-
El terminal debería decir ahora "Autenticado correctamente".
-
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, dondeoctocat
es el nombre de usuario.You are octocat
-
Abre el archivo en el editor
.token
y modifica el token. Ahora, el token no es válido. -
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.
-
Elimine el archivo
.token
. -
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».