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.rbest stocké, exécutez la commande suivante pour transformerapp_cli.rben exécutable :Text chmod +x app_cli.rb
chmod +x app_cli.rb -
Ajoutez cette ligne en haut de
app_cli.rbafin 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
mainqui va servir de point d’entrée. La fonction inclut une instructioncasepermettant d’effectuer différentes actions selon la commande spécifiée. Vous développerez cette instructioncaseulté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 enddef 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.rbse 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 mainDans votre terminal, dans le répertoire où
app_cli.rbest stocké, exécutez./app_cli.rb help. Cette sortie doit s’afficher :`help` is not yet definedVous pouvez également tester votre script sans commande ou avec une commande non prise en charge.
./app_cli.rb create-issuedoit par exemple générer :Unknown command `create-issue`
Ajouter une commande help
-
Ajoutez la fonction suivante
helpàapp_cli.rb. Actuellement, la fonctionhelpimprime une ligne pour indiquer aux utilisateurs que cette interface CLI prend une commande « help ». Vous développerez cette fonctionhelpultérieurement.Ruby def help puts "usage: app_cli <help>" end
def help puts "usage: app_cli <help>" end -
Mettez à jour la fonction
mainpour appeler la fonctionhelpquand la commandehelpest 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 enddef 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.rbse présente maintenant comme suit. L'ordre des fonctions n'a pas d'importance tant que l'appel de fonctionmainse 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 mainDans votre terminal, dans le répertoire où
app_cli.rbest 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_IDde 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_IDpar 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 OKou201 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 enddef 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/codeet 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) enddef 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_tokenet 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) enddef 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_tokenau 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 enddef 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
loginsuivante.Cette fonction :
- Appelle la fonction
request_device_codeet obtient les paramètresverification_uri,user_code,device_codeetintervaldans la réponse. - Invite les utilisateurs à entrer le
user_codede l’étape précédente. - Appelle
poll_for_tokenpour 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!" enddef 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
mainpour appeler la fonctionloginquand la commandeloginest 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 enddef 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
helppour 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.rbressemble maintenant à ceci, oùYOUR_CLIENT_IDest l’ID client de votre application. L'ordre des fonctions n'a pas d'importance tant que l'appel de fonctionmainse 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.rbest 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.tokenest 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"]}" enddef 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_responsepour 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 enddef 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
mainpour appeler la fonctionwhoamiquand la commandewhoamiest fournie :Ruby def main case ARGV[0] when "help" help when "login" login when "whoami" whoami else puts "Unknown command #{ARGV[0]}" end enddef 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
helppour 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.rbest 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.rbest 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.rbest stocké, exécutez./app_cli.rb whoami. Une sortie de ce type doit s’afficher, oùoctocatest votre nom d’utilisateur.You are octocat -
Ouvrez le fichier
.tokendans votre éditeur et modifiez le jeton. Le jeton est désormais non valide. -
Dans votre terminal, dans le répertoire où
app_cli.rbest 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.rbest 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 ».