Introduction
Ce tutoriel montre la procédure pour créer une interface de ligne de commande (CLI) qui s’appuie sur une GitHub App, et la procédure pour utiliser le flux d’appareil afin de générer un jeton d’accès utilisateur pour l’application.
L’interface CLI a trois commandes :
help
: génère les instructions d’utilisation.login
: génère un jeton d’accès utilisateur que l’application peut utiliser pour effectuer des demandes d’API au nom de l’utilisateur.whoami
: retourne des informations sur l’utilisateur connecté.
Ce tutoriel utilise Ruby, mais vous pouvez écrire une interface CLI et utiliser le flux d’appareil pour générer un jeton d’accès utilisateur avec le langage de programmation de votre choix.
À propos du flux d’appareil et des jetons d’accès utilisateur
L’interface CLI utilise le flux d’appareil pour authentifier un utilisateur et générer un jeton d’accès utilisateur. L’interface CLI peut ensuite utiliser le jeton d’accès utilisateur afin d’effectuer des demandes d’API pour le compte de l’utilisateur authentifié.
Votre application doit utiliser un jeton d’accès utilisateur si vous souhaitez attribuer des actions de l’application à un utilisateur. Pour plus d’informations, consultez « Authentification auprès d’une application GitHub pour le compte d’un utilisateur ».
Deux méthodes permettent de générer un jeton d’accès utilisateur pour une GitHub App : le flux d’application web et le flux d’appareil. Vous devez utiliser le flux d’appareil pour générer un jeton d’accès utilisateur si votre application est sans périphérique de contrôle ou n’a pas accès à un navigateur. Les outils CLI, les Raspberry Pi simples et les applications de bureau doivent par exemple utiliser le flux d’appareil. Si votre application a accès à une interface web, vous devez utiliser le flux d’application web à la place. Pour plus d’informations, consultez « Génération d’un jeton d’accès utilisateur pour une application GitHub » et « Création d’un bouton « Se connecter avec GitHub » avec une application GitHub App ».
Prérequis
Ce tutoriel part du principe que vous avez déjà inscrit une GitHub App. Pour plus d’informations sur l’inscription d’une GitHub App, consultez « Inscription d’une application GitHub ».
Avant de suivre ce tutoriel, vous devez activer le flux d’appareil pour votre application. Pour plus d’informations sur l’activation du flux d’appareil pour votre application, consultez « Modification d’une inscription d’application GitHub ».
Ce tutoriel suppose que vous disposez de connaissances de base de Ruby. Pour plus d’informations, consultez Ruby.
Obtenir l’ID client
Vous avez besoin de l’ID client de votre application pour générer un jeton d’accès utilisateur via le flux d’appareil.
- Dans le coin supérieur droit de n’importe quelle page sur GitHub, cliquez sur votre photo de profil.
- Accédez aux paramètres de votre compte.
- Pour une application appartenant à un compte personnel, cliquez sur Paramètres.
- Pour une application appartenant à une organisation :
- Cliquez sur Vos organisations.
- À droite de l’organisation, cliquez sur Paramètres.
- Dans la barre latérale gauche, cliquez sur Paramètres de développeur.
- Dans la barre latérale à gauche, cliquez sur GitHub Apps .
- À côté de l’GitHub App que vous souhaitez utiliser, cliquez sur Modifier.
- Dans la page des paramètres de l’application, recherchez l’ID client de votre application. Vous l’utiliserez ultérieurement dans ce tutoriel. Notez que l’ID client est différent de l’ID de l’application.
Écrire l’interface CLI
Ces étapes vous guident dans la création d’une interface CLI et l’utilisation du flux d’appareil pour obtenir un jeton d’accès utilisateur. Pour passer au code final, consultez « Exemple de code complet ».
Programme d’installation
-
Créez un fichier Ruby pour contenir le code qui va générer un jeton d’accès utilisateur. Dans ce tutoriel, le fichier est nommé
app_cli.rb
. -
Dans votre terminal, à partir du répertoire où
app_cli.rb
est stocké, exécutez la commande suivante pour transformerapp_cli.rb
en exécutable :Text chmod +x app_cli.rb
chmod +x app_cli.rb
-
Ajoutez cette ligne en haut de
app_cli.rb
afin d’indiquer que l’interpréteur Ruby doit être utilisé pour exécuter le script :Ruby #!/usr/bin/env ruby
#!/usr/bin/env ruby
-
Ajoutez ces dépendances en haut de
app_cli.rb
, comme suit#!/usr/bin/env ruby
:Ruby require "net/http" require "json" require "uri" require "fileutils"
require "net/http" require "json" require "uri" require "fileutils"
Ces éléments font tous partie de la bibliothèque standard Ruby. Vous n’avez donc pas besoin d’installer de gems.
-
Ajoutez la fonction suivante
main
qui va servir de point d’entrée. La fonction inclut une instructioncase
permettant d’effectuer différentes actions selon la commande spécifiée. Vous développerez cette instructioncase
ultérieurement.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 bas du fichier, ajoutez la ligne suivante pour appeler la fonction de point d’entrée. Cet appel de fonction doit rester en bas du fichier quand vous ajoutez des fonctions supplémentaires à ce fichier ultérieurement au cours de ce tutoriel.
Ruby main
main
-
Vous pouvez également vérifier votre progression :
app_cli.rb
se présente maintenant comme suit :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
Dans votre terminal, dans le répertoire où
app_cli.rb
est stocké, exécutez./app_cli.rb help
. Cette sortie doit s’afficher :`help` is not yet defined
Vous pouvez également tester votre script sans commande ou avec une commande non prise en charge.
./app_cli.rb create-issue
doit par exemple générer :Unknown command `create-issue`
Ajouter une commande help
-
Ajoutez la fonction suivante
help
àapp_cli.rb
. Actuellement, la fonctionhelp
imprime une ligne pour indiquer aux utilisateurs que cette interface CLI prend une commande « help ». Vous développerez cette fonctionhelp
ultérieurement.Ruby def help puts "usage: app_cli <help>" end
def help puts "usage: app_cli <help>" end
-
Mettez à jour la fonction
main
pour appeler la fonctionhelp
quand la commandehelp
est fournie :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
-
Vous pouvez éventuellement voir votre progression :
app_cli.rb
se présente maintenant comme suit. L'ordre des fonctions n'a pas d'importance tant que l'appel de fonctionmain
se trouve à la fin du fichier.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
Dans votre terminal, dans le répertoire où
app_cli.rb
est stocké, exécutez./app_cli.rb help
. Cette sortie doit s’afficher :usage: app_cli <help>
Ajouter une commande login
La commande login
exécute le flux d’appareil pour obtenir un jeton d’accès utilisateur. Pour plus d’informations, consultez « Génération d’un jeton d’accès utilisateur pour une application GitHub ».
-
En haut de votre fichier, après les instructions
require
, ajoutez leCLIENT_ID
de votre GitHub App en tant que constante dansapp_cli.rb
. Pour plus d’informations sur la recherche de l’ID client de votre application, consultez « Obtenir l’ID client ». RemplacezYOUR_CLIENT_ID
par l’ID client de votre application :Ruby CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_ID="YOUR_CLIENT_ID"
-
Ajoutez la fonction suivante
parse_response
àapp_cli.rb
. Cette fonction analyse une réponse de l’API REST GitHub. Quand l’état de la réponse est200 OK
ou201 Created
, la fonction retourne le corps de la réponse analysé. Dans les autres cas, la fonction imprime la réponse et le corps, puis quitte le programme.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
-
Ajoutez la fonction suivante
request_device_code
àapp_cli.rb
. Cette fonction envoie une requêtePOST
àhttp(s)://HOSTNAME/login/device/code
et retourne la réponse.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
-
Ajoutez la fonction suivante
request_token
àapp_cli.rb
. Cette fonction envoie une requêtePOST
àhttp(s)://HOSTNAME/login/oauth/access_token
et retourne la réponse.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
-
Ajoutez la fonction suivante
poll_for_token
àapp_cli.rb
. Cette fonction interrogehttp(s)://HOSTNAME/login/oauth/access_token
à l’intervalle spécifié jusqu’à ce que GitHub réponde avec un paramètreaccess_token
au lieu d’un paramètreerror
. Elle écrit ensuite le jeton d’accès utilisateur dans un fichier et limite les autorisations sur le fichier.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
-
Ajoutez la fonction
login
suivante.Cette fonction :
- Appelle la fonction
request_device_code
et obtient les paramètresverification_uri
,user_code
,device_code
etinterval
dans la réponse. - Invite les utilisateurs à entrer le
user_code
de l’étape précédente. - Appelle
poll_for_token
pour interroger GitHub afin d’obtenir un jeton d’accès. - Indique à l’utilisateur que l’authentification a réussi.
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
- Appelle la fonction
-
Mettez à jour la fonction
main
pour appeler la fonctionlogin
quand la commandelogin
est fournie :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
-
Mettez à jour la fonction
help
pour inclure la commandelogin
:Ruby def help puts "usage: app_cli <login | help>" end
def help puts "usage: app_cli <login | help>" end
-
Vous pouvez également vérifier votre progression :
app_cli.rb
ressemble maintenant à ceci, oùYOUR_CLIENT_ID
est l’ID client de votre application. L'ordre des fonctions n'a pas d'importance tant que l'appel de fonctionmain
se trouve à la fin du fichier.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
-
Dans votre terminal, dans le répertoire où
app_cli.rb
est stocké, exécutez./app_cli.rb login
. Vous devez obtenir une sortie similaire à celle-ci. Le code est différent à chaque fois :Please visit: http(s)://HOSTNAME/login/device and enter code: CA86-8D94
-
Accédez à http(s)://HOSTNAME/login/device dans votre navigateur, entrez le code de l’étape précédente, puis cliquez sur Continuer.
-
GitHub doit afficher une page qui vous invite à autoriser votre application. Cliquez sur le bouton « Autoriser ».
-
Votre terminal doit maintenant indiquer « Authentifié avec succès ».
-
Ajouter une commande whoami
Maintenant que votre application peut générer un jeton d’accès utilisateur, vous pouvez effectuer des demandes d’API pour le compte de l’utilisateur. Ajoutez une commande whoami
pour obtenir le nom d’utilisateur de l’utilisateur authentifié.
-
Ajoutez la fonction suivante
whoami
àapp_cli.rb
. Cette fonction obtient les informations sur l’utilisateur avec le point de terminaison de l’API REST/user
. Elle génère le nom d’utilisateur qui correspond au jeton d’accès utilisateur. Si le fichier.token
est introuvable, elle invite l’utilisateur à exécuter la fonctionlogin
.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
-
Mettez à jour la fonction
parse_response
pour gérer le cas d’un jeton qui a expiré ou a été révoqué. Si vous obtenez maintenant une réponse401 Unauthorized
, l’interface CLI invite l’utilisateur à exécuter la commandelogin
.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
-
Mettez à jour la fonction
main
pour appeler la fonctionwhoami
quand la commandewhoami
est fournie :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
-
Mettez à jour la fonction
help
pour inclure la commandewhoami
:Ruby def help puts "usage: app_cli <login | whoami | help>" end
def help puts "usage: app_cli <login | whoami | help>" end
-
Vérifiez votre code par rapport à l’exemple de code complet présenté dans la section suivante. Vous pouvez tester votre code en suivant les étapes décrites dans la section « Test » sous l’exemple de code complet.
Exemple de code complet
Il s’agit de l’exemple de code complet décrit dans la section précédente. Remplacez YOUR_CLIENT_ID
par l’ID client de votre application.
#!/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
Test
Ce tutoriel part du principe que le code de votre application est stocké dans un fichier nommé app_cli.rb
.
-
Dans votre terminal, dans le répertoire où
app_cli.rb
est stocké, exécutez./app_cli.rb help
. Vous devez obtenir une sortie similaire à celle-ci.usage: app_cli <login | whoami | help>
-
Dans votre terminal, dans le répertoire où
app_cli.rb
est stocké, exécutez./app_cli.rb login
. Vous devez obtenir une sortie similaire à celle-ci. Le code est différent à chaque fois :Please visit: http(s)://HOSTNAME/login/device and enter code: CA86-8D94
-
Accédez à http(s)://HOSTNAME/login/device dans votre navigateur, entrez le code de l’étape précédente, puis cliquez sur Continuer.
-
GitHub doit afficher une page qui vous invite à autoriser votre application. Cliquez sur le bouton « Autoriser ».
-
Votre terminal doit maintenant indiquer « Authentifié avec succès ».
-
Dans votre terminal, dans le répertoire où
app_cli.rb
est stocké, exécutez./app_cli.rb whoami
. Une sortie de ce type doit s’afficher, oùoctocat
est votre nom d’utilisateur.You are octocat
-
Ouvrez le fichier
.token
dans votre éditeur et modifiez le jeton. Le jeton est désormais non valide. -
Dans votre terminal, dans le répertoire où
app_cli.rb
est stocké, exécutez./app_cli.rb whoami
. Vous devez normalement voir une sortie similaire à celle-ci :You are not authorized. Run the `login` command.
-
Supprimez le fichier
.token
. -
Dans votre terminal, dans le répertoire où
app_cli.rb
est stocké, exécutez./app_cli.rb whoami
. Vous devez normalement voir une sortie similaire à celle-ci :You are not authorized. Run the `login` command.
Étapes suivantes
Ajustez le code selon les besoins de votre application
Ce tutoriel a montré comment écrire une interface CLI qui utilise le flux d’appareil pour générer un jeton d’accès utilisateur. Vous pouvez développer cette interface CLI pour accepter des commandes supplémentaires. Vous pouvez par exemple ajouter une commande create-issue
qui ouvre un problème. N’oubliez pas de mettre à jour les autorisations de votre application si des autorisations supplémentaires sont nécessaires pour les demandes d’API que vous souhaitez effectuer. Pour plus d’informations, consultez « Choix des autorisations pour une application GitHub ».
Stocker les jetons en toute sécurité
Ce tutoriel génère un jeton d’accès utilisateur et l’enregistre dans un fichier local. Vous ne devez jamais valider (commit) ce fichier ni diffuser le jeton.
Selon votre appareil, vous pouvez choisir d'autres méthodes de stockage du jeton. Vous devez vérifier les meilleures pratiques pour stocker des jetons sur votre appareil.
Pour plus d’informations, consultez « Meilleures pratiques pour la création d’une application GitHub ».
Suivre les bonnes pratiques
Vous devez vous efforcer de suivre les bonnes pratiques avec votre GitHub App. Pour plus d’informations, consultez « Meilleures pratiques pour la création d’une application GitHub ».