Введение
В этом руководстве показано, как создать интерфейс командной строки (CLI) на основе GitHub App, а также как использовать поток устройства для создания маркера доступа пользователя для приложения.
Интерфейс командной строки содержит три команды:
help
: выводит инструкции по использованию.login
: создает маркер доступа пользователя, который приложение может использовать для выполнения запросов API от имени пользователя.whoami
: возвращает сведения о вошедшего пользователя.
В этом руководстве используется Ruby, но вы можете написать интерфейс командной строки и использовать поток устройства для создания маркера доступа пользователя на любом языке программирования.
Сведения о потоке устройства и маркерах доступа пользователей
Cli будет использовать поток устройства для проверки подлинности пользователя и создания маркера доступа пользователя. Затем CLI может использовать маркер доступа пользователя для выполнения запросов API от имени пользователя, прошедшего проверку подлинности.
Приложение должно использовать маркер доступа пользователя, если вы хотите приказать действия приложения пользователю. Дополнительные сведения см. в разделе Проверка подлинности с помощью Приложение GitHub от имени пользователя.
Существует два способа создания маркера доступа пользователя для GitHub App: поток веб-приложения и поток устройства. Для создания маркера доступа пользователя следует использовать поток устройства, если приложение не имеет доступа к веб-интерфейсу. Например, средства CLI, простые Raspberry Pis и классические приложения должны использовать поток устройства. Если приложение имеет доступ к веб-интерфейсу, следует использовать поток веб-приложения. Дополнительные сведения см. в разделах Создание маркера доступа пользователя для Приложение GitHub и Создание кнопки "Вход с помощью GitHub" с Приложение GitHub.
Предварительные требования
В этом руководстве предполагается, что вы уже создали GitHub App. Дополнительные сведения о создании приложения см. в разделе Создание приложения GitHub.
Перед выполнением этого руководства необходимо включить поток устройств для приложения. Дополнительные сведения о включении потока устройств для приложения см. в разделе Изменение приложения GitHub.
В этом руководстве предполагается, что у вас есть базовое представление о Ruby. Дополнительные сведения см. в разделе Ruby.
Получение идентификатора клиента
Вам потребуется идентификатор клиента приложения, чтобы создать маркер доступа пользователя через поток устройства.
-
Перейдите к настройкам учетной записи.
-
Для приложения GitHub App, принадлежащего учетной записи пользователя, нажмите на фото своего профиля в правом верхнем углу любой страницы и выберите Настройки.
-
Для приложения GitHub App, принадлежащего организации, нажмите на фото своего профиля в правом верхнем углу любой страницы и выберите Ваши организации. Затем нажмите Настройки справа от организации.
-
-
На левой боковой панели щелкните Параметры разработчика.
-
На левой боковой панели щелкните GitHub Apps.
-
Рядом с GitHub App, с которым вы хотите работать, нажмите кнопку Изменить.
-
На странице параметров приложения найдите идентификатор клиента для приложения. Вы будете использовать его далее в этом руководстве. Обратите внимание, что идентификатор клиента отличается от идентификатора приложения.
Запись интерфейса командной строки
Эти шаги поведут вас к созданию ИНТЕРФЕЙСА командной строки и использованию потока устройства для получения маркера доступа пользователя. Чтобы перейти к окончательному коду, см. раздел Полный пример кода.
Настройка
-
Создайте файл Ruby для хранения кода, который создаст маркер доступа пользователя. В этом руководстве файлу
app_cli.rb
будет присвоено имя . -
В окне терминала в каталоге, в котором
app_cli.rb
хранится файл, выполните следующую команду, чтобы сделатьapp_cli.rb
исполняемый файл:Code chmod +x app_cli.rb
-
Добавьте эту строку в начало, чтобы указать, что для выполнения скрипта
app_cli.rb
следует использовать интерпретатор Ruby:Ruby #!/usr/bin/env ruby
-
Добавьте эти зависимости в начало , следующее
app_cli.rb``#!/usr/bin/env ruby
:Ruby require "net/http" require "json" require "uri" require "fileutils"
Все они являются частью стандартной библиотеки Ruby, поэтому вам не нужно устанавливать какие-либо драгоценные камни.
-
Добавьте следующую
main
функцию, которая будет служить точкой входа. Функция включает инструкциюcase
для выполнения различных действий в зависимости от указанной команды. Вы развернете этуcase
инструкцию позже.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
-
В нижней части файла добавьте следующую строку, чтобы вызвать функцию точки входа. Этот вызов функции должен оставаться в нижней части файла, так как вы добавите дополнительные функции в этот файл далее в этом руководстве.
Ruby main
-
При необходимости проверка хода выполнения:
app_cli.rb
теперь выглядит следующим образом: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
В терминале из каталога, в котором
app_cli.rb
хранится, выполните команду./app_cli.rb help
. Вы должны увидеть такой результат:`help` is not yet defined
Вы также можете протестировать скрипт без команды или с помощью необработанных команд. Например,
./app_cli.rb create-issue
должен выводить следующее:Unknown command `create-issue`
help
Добавление команды
-
Добавьте следующую
help
функцию вapp_cli.rb
. В настоящее время функция выводит строку,help
чтобы сообщить пользователям, что этот интерфейс командной строки принимает одну команду help. Вы развернете этуhelp
функцию позже.Ruby def help puts "usage: app_cli <help>" end
-
Обновите функцию
main
для вызоваhelp
функции при выполненииhelp
команды:Ruby def main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end
-
При необходимости проверка хода выполнения:
app_cli.rb
теперь выглядит следующим образом. Порядок функций не имеет значения, еслиmain
вызов функции находится в конце файла.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
В терминале из каталога, в котором
app_cli.rb
хранится, выполните команду./app_cli.rb help
. Вы должны увидеть такой результат:usage: app_cli <help>
login
Добавление команды
Команда login
запустит поток устройства, чтобы получить маркер доступа пользователя. Дополнительные сведения см. в разделе Создание маркера доступа пользователя для Приложение GitHub.
-
В верхней части файла после
require
операторов добавьтеCLIENT_ID
GitHub App в качестве константы вapp_cli.rb
. Дополнительные сведения о поиске идентификатора клиента приложения см. в разделе Получение идентификатора клиента. ЗаменитеYOUR_CLIENT_ID
идентификатором клиента приложения:Ruby CLIENT_ID="YOUR_CLIENT_ID"
-
Добавьте следующую
parse_response
функцию вapp_cli.rb
. Эта функция анализирует ответ из REST API GitHub. Если состояние ответа равно200 OK
или201 Created
, функция возвращает проанализированный текст ответа. В противном случае функция выводит ответ, а текст завершает программу.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
-
Добавьте следующую
request_device_code
функцию вapp_cli.rb
. Эта функция отправляетPOST
запрос иhttp(s)://HOSTNAME/login/device/code
возвращает ответ.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
-
Добавьте следующую
request_token
функцию вapp_cli.rb
. Эта функция отправляетPOST
запрос иhttp(s)://HOSTNAME/login/oauth/access_token
возвращает ответ.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
-
Добавьте следующую
poll_for_token
функцию вapp_cli.rb
. Эта функция опрашиваетhttp(s)://HOSTNAME/login/oauth/access_token
с указанным интервалом, пока GitHub не ответитaccess_token
параметром, а не параметромerror
. Затем он записывает маркер доступа пользователя в файл и ограничивает разрешения для файла.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
-
Добавьте следующую
login
функцию.Эта функция выполняет следующее:
- Вызывает функцию
request_device_code
и получаетverification_uri
параметры ,user_code
,device_code
иinterval
из ответа. - Предлагает пользователям ввести из
user_code
предыдущего шага. - Вызывает для опроса маркера
poll_for_token
доступа GitHub. - Сообщает пользователю, что проверка подлинности прошла успешно.
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
- Вызывает функцию
-
Обновите функцию
main
для вызоваlogin
функции при выполненииlogin
команды:Ruby def main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end
-
Обновите функцию
help
, включив командуlogin
:Ruby def help puts "usage: app_cli <login | help>" end
-
При необходимости проверка хода выполнения:
app_cli.rb
теперь выглядит примерно так, гдеYOUR_CLIENT_ID
— это идентификатор клиента вашего приложения. Порядок функций не имеет значения, еслиmain
вызов функции находится в конце файла.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
-
В терминале из каталога, в котором
app_cli.rb
хранится, выполните команду./app_cli.rb login
. Вы должны увидеть выходные данные, которые выглядят следующим образом. Код будет отличаться каждый раз:Please visit: http(s)://HOSTNAME/login/device and enter code: CA86-8D94
-
Перейдите в http(s)://HOSTNAME/login/device в браузере и введите код из предыдущего шага, а затем нажмите кнопку Продолжить.
-
GitHub должна отображать страницу с предложением авторизовать приложение. Нажмите кнопку "Авторизовать".
-
Теперь в терминале должно быть указано "Успешно выполнена проверка подлинности!".
-
whoami
Добавление команды
Теперь, когда приложение может создать маркер доступа пользователя, можно выполнять запросы API от имени пользователя. whoami
Добавьте команду, чтобы получить имя пользователя, прошедшего проверку подлинности.
-
Добавьте следующую
whoami
функцию вapp_cli.rb
. Эта функция получает сведения о пользователе с помощью конечной/user
точки REST API. Он выводит имя пользователя, соответствующее маркеру доступа пользователя..token
Если файл не найден, пользователю предлагается выполнить функциюlogin
.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
-
Обновите функцию
parse_response
для обработки случаев, когда срок действия маркера истек или был отозван. Теперь, если вы получите401 Unauthorized
ответ, интерфейс командной строки предложит пользователю выполнитьlogin
команду.Ruby def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) when Net::HTTPUnauthorized puts "You are not authorized. Run the `login` command." exit 1 else puts response puts response.body exit 1 end end
-
Обновите функцию
main
для вызоваwhoami
функции при выполненииwhoami
команды:Ruby def main case ARGV[0] when "help" help when "login" login when "whoami" whoami else puts "Unknown command #{ARGV[0]}" end end
-
Обновите функцию,
help
включив командуwhoami
:Ruby def help puts "usage: app_cli <login | whoami | help>" end
-
Проверьте код на соответствие полному примеру кода в следующем разделе. Вы можете протестировать код, выполнив действия, описанные в разделе "Тестирование" под полным примером кода.
Полный пример кода
Это полный пример кода, описанный в предыдущем разделе. Замените YOUR_CLIENT_ID
идентификатором клиента приложения.
#!/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
Тестирование
В этом руководстве предполагается, что код приложения хранится в файле с именем app_cli.rb
.
-
В терминале в каталоге, где
app_cli.rb
хранится , выполните команду./app_cli.rb help
. Должны отобразиться выходные данные, которые выглядят следующим образом.usage: app_cli <login | whoami | help>
-
В терминале в каталоге, где
app_cli.rb
хранится , выполните команду./app_cli.rb login
. Должны отобразиться выходные данные, которые выглядят следующим образом. Код будет отличаться каждый раз:Please visit: http(s)://HOSTNAME/login/device and enter code: CA86-8D94
-
Перейдите к http(s)://HOSTNAME/login/device в браузере и введите код из предыдущего шага, а затем нажмите кнопку Продолжить.
-
В GitHub должна отображаться страница с запросом на авторизацию приложения. Нажмите кнопку "Авторизовать".
-
В окне терминала должно появиться сообщение "Проверка подлинности успешно выполнена!".
-
В терминале в каталоге, где
app_cli.rb
хранится , выполните команду./app_cli.rb whoami
. Вы увидите выходные данные, которые выглядят следующим образом, гдеoctocat
— ваше имя пользователя.You are octocat
-
.token
Откройте файл в редакторе и измените маркер. Теперь маркер недопустим. -
В терминале в каталоге, где
app_cli.rb
хранится , выполните команду./app_cli.rb whoami
. Вы должны получить следующий результат:You are not authorized. Run the `login` command.
-
Удалите файл
.token
. -
В терминале в каталоге, где
app_cli.rb
хранится , выполните команду./app_cli.rb whoami
. Вы должны получить следующий результат:You are not authorized. Run the `login` command.
Дальнейшие действия
Настройка кода в соответствии с потребностями приложения
В этом руководстве показано, как написать интерфейс командной строки, который использует поток устройства для создания маркера доступа пользователя. Вы можете развернуть этот интерфейс командной строки, чтобы принимать дополнительные команды. Например, можно добавить create-issue
команду, которая открывает проблему. Не забудьте обновить разрешения приложения, если приложению требуются дополнительные разрешения для запросов API, которые вы хотите выполнить. Дополнительные сведения см. в разделе Выбор разрешений для Приложение GitHub.
Безопасное хранение маркеров
В этом руководстве создается маркер доступа пользователя и он сохраняется в локальном файле. Никогда не следует фиксировать этот файл или делать его общедоступным.
В зависимости от устройства вы можете выбрать другой способ хранения маркера. Следует проверка рекомендации по хранению маркеров на устройстве.
Дополнительные сведения см. в разделе Рекомендации по созданию Приложение GitHub.