From fe06e035c033c38c84fcb5d3685b3cee4f933e5a Mon Sep 17 00:00:00 2001 From: Sarah Edwards Date: Fri, 31 Mar 2023 12:24:23 -0700 Subject: [PATCH] Add device flow tutorial (#35970) --- ...ng-a-user-access-token-for-a-github-app.md | 32 +- .../refreshing-user-access-tokens.md | 2 +- .../building-a-cli-with-a-github-app.md | 750 ++++++++++++++++++ ...n-with-github-button-with-a-github-app.md} | 21 +- .../apps/creating-github-apps/guides/index.md | 6 +- .../apps/web-app-flow-token-response.md | 2 +- 6 files changed, 785 insertions(+), 28 deletions(-) create mode 100644 content/apps/creating-github-apps/guides/building-a-cli-with-a-github-app.md rename content/apps/creating-github-apps/guides/{using-the-web-application-flow-to-generate-a-user-access-token-for-a-github-app.md => building-a-login-with-github-button-with-a-github-app.md} (91%) diff --git a/content/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.md b/content/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.md index 6f5937bee6..aab3529be7 100644 --- a/content/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.md +++ b/content/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.md @@ -57,7 +57,7 @@ If your app runs in the browser, you should use the web application flow to gene {% endnote %} -If your app is headless or does not have access to a browser, you should use the device flow to generate a user access token. For example, CLI tools or Git credential mangers should use the device flow. +If your app is headless or does not have access to a browser, you should use the device flow to generate a user access token. For example, CLI tools, simple Raspberry Pis, and desktop applications should use the device flow. {% ifversion device-flow-is-opt-in %}Before you can use the device flow, you must first enable it in your app's settings. For more information on enabling device flow, see "[AUTOTITLE](/apps/maintaining-github-apps/modifying-a-github-app)." {% endif %} @@ -69,7 +69,7 @@ The device flow uses the OAuth 2.0 Device Authorization Grant. Replace `APP-SLUG` with the slugified name of your app and `ORGANIZATION` with the slugified name of your organization. For example, `https://github.com/organizations/octo-org/settings/apps/octo-app`. -1. {% data variables.product.company_short %} will respond with a response that includes the following parameters: +1. {% data variables.product.company_short %} will give a response that includes the following query parameters: Response parameter | Type | Description --- | --- | --- @@ -91,25 +91,25 @@ The device flow uses the OAuth 2.0 Device Authorization Grant. Do not poll this endpoint at a higher frequency than the frequency indicated by `interval`. If you do, you will hit the rate limit and receive a `slow_down` error. The `slow_down` error response adds 5 seconds to the last `interval`. -1. Once the user has entered the `user_code`, {% data variables.product.company_short %} will respond with a response body that includes the following parameters: + Until the user enters the code, {% data variables.product.company_short %} will respond with a 200 status and an `error` response query parameter. + + | Error name | Description | + |----|----| + | `authorization_pending`| This error occurs when the authorization request is pending and the user hasn't entered the user code yet. The app is expected to keep polling the `POST {% data variables.product.oauth_host_code %}/login/oauth/access_token` at a frequency no faster than the frequency specified by `interval`. + | `slow_down` | When you receive the `slow_down` error, 5 extra seconds are added to the minimum `interval` or timeframe required between your requests using `POST {% data variables.product.oauth_host_code %}/login/oauth/access_token`. For example, if the starting interval required at least 5 seconds between requests and you get a `slow_down` error response, you must now wait a minimum of 10 seconds before making a new request for a token. The error response includes the new `interval` that you must use. + | `expired_token` | If the device code expired, then you will see the `token_expired` error. You must make a new request for a device code. + | `unsupported_grant_type` | The grant type must be `urn:ietf:params:oauth:grant-type:device_code` and included as an input parameter when you poll the OAuth token request `POST {% data variables.product.oauth_host_code %}/login/oauth/access_token`. + | `incorrect_client_credentials` | For the device flow, you must pass your app's client ID, which you can find on your app settings page. The client ID is different from the app ID and client secret. + | `incorrect_device_code` | The `device_code` provided is not valid. + | `access_denied` | When a user clicks cancel during the authorization process, you'll receive an `access_denied` error, and the user won't be able to use the verification code again.{% ifversion device-flow-is-opt-in %} + | `device_flow_disabled` | Device flow has not been enabled in the app's settings. For more information on enabling device flow, see "[AUTOTITLE](/apps/maintaining-github-apps/modifying-a-github-app)."{% endif %} + +1. Once the user has entered the `user_code`, {% data variables.product.company_short %} will give a response that includes the following query parameters: {% data reusables.apps.user-access-token-response-parameters %} {% data reusables.apps.user-access-token-example-request %} -### Error codes for the device flow - -| Error code | Description | -|----|----| -| `authorization_pending`| This error occurs when the authorization request is pending and the user hasn't entered the user code yet. The app is expected to keep polling the `POST {% data variables.product.oauth_host_code %}/login/oauth/access_token` at a frequency no faster than the frequency specified by `interval`. -| `slow_down` | When you receive the `slow_down` error, 5 extra seconds are added to the minimum `interval` or timeframe required between your requests using `POST {% data variables.product.oauth_host_code %}/login/oauth/access_token`. For example, if the starting interval required at least 5 seconds between requests and you get a `slow_down` error response, you must now wait a minimum of 10 seconds before making a new request for a token. The error response includes the new `interval` that you must use. -| `expired_token` | If the device code expired, then you will see the `token_expired` error. You must make a new request for a device code. -| `unsupported_grant_type` | The grant type must be `urn:ietf:params:oauth:grant-type:device_code` and included as an input parameter when you poll the OAuth token request `POST {% data variables.product.oauth_host_code %}/login/oauth/access_token`. -| `incorrect_client_credentials` | For the device flow, you must pass your app's client ID, which you can find on your app settings page. The client ID is different from the app ID and client secret. -| `incorrect_device_code` | The `device_code` provided is not valid. -| `access_denied` | When a user clicks cancel during the authorization process, you'll receive an `access_denied` error, and the user won't be able to use the verification code again.{% ifversion device-flow-is-opt-in %} -| `device_flow_disabled` | Device flow has not been enabled in the app's settings. For more information on enabling device flow, see "[AUTOTITLE](/apps/maintaining-github-apps/modifying-a-github-app)."{% endif %} - ## Generating a user access token when a user installs your app If you select **Request user authorization (OAuth) during installation** in your app settings, {% data variables.product.company_short %} will start the web application flow immediately after a user installs your app. diff --git a/content/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens.md b/content/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens.md index 867f958001..2e4cf463fb 100644 --- a/content/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens.md +++ b/content/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens.md @@ -55,6 +55,6 @@ If you opt into user access tokens that expire after you have already generated `grant_type` | `string` | **Required.** The value must be "refresh_token". `refresh_token` | `string` | **Required.** The refresh token that you received when you generated a user access token. -1. {% data variables.product.company_short %} will respond with a response body that includes the following parameters: +1. {% data variables.product.company_short %} will give a response that includes the following parameters: {% data reusables.apps.user-access-token-response-parameters %} diff --git a/content/apps/creating-github-apps/guides/building-a-cli-with-a-github-app.md b/content/apps/creating-github-apps/guides/building-a-cli-with-a-github-app.md new file mode 100644 index 0000000000..8768aac8f9 --- /dev/null +++ b/content/apps/creating-github-apps/guides/building-a-cli-with-a-github-app.md @@ -0,0 +1,750 @@ +--- +title: Building a CLI with a GitHub App +shortTitle: Build a CLI +intro: "Follow this tutorial to write a CLI in Ruby that generates a user access token for a {% data variables.product.prodname_github_app %} via the device flow." +versions: + fpt: '*' + ghes: '*' + ghae: '*' + ghec: '*' +topics: + - GitHub Apps +--- + +## Introduction + +This tutorial demonstrates how to build a command line interface (CLI) backed by a {% data variables.product.prodname_github_app %}, and how to use the device flow to generate a user access token for the app. + +The CLI will have three commands: + +- `help`: Outputs the usage instructions. +- `login`: Generates a user access token that the app can use to make API requests on behalf of the user. +- `whoami`: Returns information about the logged in user. + +This tutorial uses Ruby, but you can write a CLI and use the device flow to generate a user access token with any programming language. + +### About device flow and user access tokens + +The CLI will use the device flow to authenticate a user and generate a user access token. Then, the CLI can use the user access token to make API requests on behalf of the authenticated user. + +Your app should use a user access token if you want to attribute the app's actions to a user. For more information, see "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-with-a-github-app-on-behalf-of-a-user)." + +There are two ways to generate a user access token for a {% data variables.product.prodname_github_app %}: web application flow and device flow. You should use the device flow to generate a user access token if your app is headless or does not have access to a web interface. For example, CLI tools, simple Raspberry Pis, and desktop applications should use the device flow. If your app has access to a web interface, you should use web application flow instead. For more information, see "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app)" and "[AUTOTITLE](/apps/creating-github-apps/guides/using-the-web-application-flow-to-generate-a-user-access-token-for-a-github-app)." + +## Prerequisites + +This tutorial assumes that you have already created a {% data variables.product.prodname_github_app %}. For more information about creating an app, see "[AUTOTITLE](/apps/creating-github-apps/creating-github-apps/creating-a-github-app)." + +Before following this tutorial, you must enable device flow for your app. For more information about enabling device flow for your app, see "[AUTOTITLE](/apps/maintaining-github-apps/modifying-a-github-app)." + +This tutorial assumes that you have a basic understanding of Ruby. For more information, see [Ruby](https://www.ruby-lang.org). + +## Get the client ID + +You will need your app's client ID in order to generate a user access token via the device flow. + +{% data reusables.apps.settings-step %} +{% data reusables.user-settings.developer_settings %} +{% data reusables.user-settings.github_apps %} +1. Next to the {% data variables.product.prodname_github_app %} that you want to work with, click **Edit**. +1. On the app's settings page, find the client ID for your app. You will use it later in this tutorial. Note that the client ID is different from the app ID. + +## Write the CLI + +These steps lead you through building a CLI and using device flow to get a user access token. To skip ahead to the final code, see "[Full code example](#full-code-example)." + +### Setup + +1. Create a Ruby file to hold the code that will generate a user access token. This tutorial will name the file `app_cli.rb`. +1. In your terminal, from the directory where `app_cli.rb` is stored, run the following command to make `app_cli.rb` executable: + + ```{:copy} + chmod +x app_cli.rb + ``` + +1. Add this line to the top of `app_cli.rb` to indicate that the Ruby interpreter should be used to run the script: + + ```ruby{:copy} + #!/usr/bin/env ruby + ``` + +1. Add these dependencies to the top of `app_cli.rb`, following `#!/usr/bin/env ruby`: + + ```ruby{:copy} + require "net/http" + require "json" + require "uri" + require "fileutils" + ``` + + These are all part of the Ruby standard library, so you don't need to install any gems. +1. Add the following `main` function that will serve as an entry point. The function includes a `case` statement to take different actions depending on which command is specified. You will expand this `case` statement later. + + ```ruby{:copy} + 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 + ``` + +1. At the bottom of the file, add the following line to call the entry point function. This function call should remain at the bottom of your file as you add more functions to this file later in the tutorial. + + ```ruby{:copy} + main + ``` + +1. Optionally, check your progress: + + `app_cli.rb` now looks like this: + + ```ruby{:copy} + #!/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 + ``` + + In your terminal, from the directory where `app_cli.rb` is stored, run `./app_cli.rb help`. You should see this output: + + ``` + `help` is not yet defined + ``` + + You can also test your script without a command or with an unhandled command. For example, `./app_cli.rb create-issue` should output: + + ``` + Unknown command `create-issue` + ``` + +### Add a `help` command + +1. Add the following `help` function to `app_cli.rb`. Currently, the `help` function prints a line to tell users that this CLI takes one command, "help". You will expand this `help` function later. + + ```ruby{:copy} + def help + puts "usage: app_cli " + end + ``` + +1. Update the `main` method to call the `help` function when the `help` command is given: + + ```ruby{:copy} + 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 + ``` + +1. Optionally, check your progress: + + `app_cli.rb` now looks like this. The order of the functions don't matter as long as the `main` function call is at the end of the file. + + ```ruby{:copy} + #!/usr/bin/env ruby + + require "net/http" + require "json" + require "uri" + require "fileutils" + + def help + puts "usage: app_cli " + 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 + ``` + + In your terminal, from the directory where `app_cli.rb` is stored, run `./app_cli.rb help`. You should see this output: + + ``` + usage: app_cli + ``` + +### Add a `login` command + +The `login` command will run the device flow to get a user access token. For more information, see "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-device-flow-to-generate-a-user-access-token)." + +1. Near the top of your file, after the `require` statements, add the `CLIENT_ID` of your {% data variables.product.prodname_github_app %} as a constant in `app_cli.rb`. For more information about finding your app's client ID, see "[Get the client ID](#get-the-client-id)." Replace `YOUR_CLIENT_ID` with the client ID of your app: + + ```ruby{:copy} + CLIENT_ID="YOUR_CLIENT_ID" + ``` + +1. Add the following `parse_response` function to `app_cli.rb`. This function parses a response from the {% data variables.product.company_short %} REST API. When the response status is `200 OK` or `201 Created`, the function returns the parsed response body. Otherwise, the function prints the response and body an exits the program. + + ```ruby{:copy} + 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 + ``` + +1. Add the following `request_device_code` function to `app_cli.rb`. This function makes a `POST` request to `{% data variables.product.oauth_host_code %}/login/device/code` and returns the response. + + ```ruby{:copy} + def request_device_code + uri = URI("{% data variables.product.oauth_host_code %}/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 + ``` + +1. Add the following `request_token` function to `app_cli.rb`. This function makes a `POST` request to `{% data variables.product.oauth_host_code %}/login/oauth/access_token` and returns the response. + + ```ruby{:copy} + def request_token(device_code) + uri = URI("{% data variables.product.oauth_host_code %}/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 + ``` + +1. Add the following `poll_for_token` function to `app_cli.rb`. This function polls `{% data variables.product.oauth_host_code %}/login/oauth/access_token` at the specified interval until {% data variables.product.company_short %} responds with an `access_token` parameter instead of an `error` parameter. Then, it writes the user access token to a file and restricts the permissions on the file. + + ```ruby{:copy} + 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 + ``` + +1. Add the following `login` function. + + This function: + + 1. Calls the `request_device_code` function and gets the `verification_uri`, `user_code`, `device_code`, and `interval` parameters from the response. + 1. Prompts users to enter the `user_code` from the previous step. + 1. Calls the `poll_for_token` to poll {% data variables.product.company_short %} for an access token. + 1. Lets the user know that authentication was successful. + + ```ruby{:copy} + 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 + ``` + +1. Update the `main` method to call the `login` function when the `login` command is given: + + ```ruby{:copy} + 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 + ``` + +1. Update the `help` function to include the `login` command: + + ```ruby{:copy} + def help + puts "usage: app_cli " + end + ``` + +1. Optionally, check your progress: + + `app_cli.rb` now looks something like this, where `YOUR_CLIENT_ID` is the client ID of your app. The order of the functions don't matter as long as the `main` function call is at the end of the file. + + ```ruby{:copy} + #!/usr/bin/env ruby + + require "net/http" + require "json" + require "uri" + require "fileutils" + + CLIENT_ID="YOUR_CLIENT_ID" + + def help + puts "usage: app_cli " + 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("{% data variables.product.oauth_host_code %}/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("{% data variables.product.oauth_host_code %}/login/oauth/access_token") + parameters = URI.encode_www_form({ + "client_id" => CLIENT_ID, + "device_code" => device_code, + "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" + }) + headers = {"Accept" => "application/json"} + response = Net::HTTP.post(uri, parameters, headers) + parse_response(response) + end + + def poll_for_token(device_code, interval) + + loop do + response = request_token(device_code) + error, access_token = response.values_at("error", "access_token") + + if error + case error + when "authorization_pending" + # The user has not yet entered the code. + # Wait, then poll again. + sleep interval + next + when "slow_down" + # The app polled too fast. + # Wait for the interval plus 5 seconds, then poll again. + sleep interval + 5 + next + when "expired_token" + # The `device_code` expired, and the process needs to restart. + puts "The device code has expired. Please run `login` again." + exit 1 + when "access_denied" + # The user cancelled the process. Stop polling. + puts "Login cancelled by user." + exit 1 + else + puts response + exit 1 + end + end + + File.write("./.token", access_token) + + # Set the file permissions so that only the file owner can read or modify the file + FileUtils.chmod(0600, "./.token") + + break + end + end + + def login + verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") + + puts "Please visit: #{verification_uri}" + puts "and enter code: #{user_code}" + + poll_for_token(device_code, interval) + + puts "Successfully authenticated!" + end + + main + ``` + + 1. In your terminal, from the directory where `app_cli.rb` is stored, run `./app_cli.rb login`. You should see output that looks like this. The code will differ every time: + + ``` + Please visit: {% data variables.product.oauth_host_code %}/login/device + and enter code: CA86-8D94 + ``` + + 1. Navigate to {% data variables.product.oauth_host_code %}/login/device in your browser and enter the code from the previous step, then click **Continue**. + 1. {% data variables.product.company_short %} should display a page that prompts you to authorize your app. Click the "Authorize" button. + 1. Your terminal should now say "Successfully authenticated!". + +### Add a `whoami` command + +Now that your app can generate a user access token, you can make API requests on behalf of the user. Add a `whoami` command to get the username of the authenticated user. + +1. Add the following `whoami` function to `app_cli.rb`. This function gets information about the user with the `/user` REST API endpoint. It outputs the username that corresponds to the user access token. If the `.token` file was not found, it prompts the user to run the `login` method. + + ```ruby{:copy} + def whoami + uri = URI("{% data variables.product.api_url_code %}/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 + ``` + +1. Update the `parse_response` function to handle the case where the token has expired or been revoked. Now, if you get a `401 Unauthorized` response, the CLI will prompt the user to run the `login` command. + + ```ruby{:copy} + 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 + ``` + +1. Update the `main` method to call the `whoami` function when the `whoami` command is given: + + ```ruby{:copy} + def main + case ARGV[0] + when "help" + help + when "login" + login + when "whoami" + whoami + else + puts "Unknown command #{ARGV[0]}" + end + end + ``` + +1. Update the `help` function to include the `whoami` command: + + ```ruby{:copy} + def help + puts "usage: app_cli " + end + ``` + +1. Check your code against the full code example in the next section. You can test your code by following the steps outlined in the "[Testing](#testing)" section below the full code example. + +## Full code example + +This is the full code example that was outlined in the previous section. Replace `YOUR_CLIENT_ID` with the client ID of your app. + + ```ruby{:copy} + #!/usr/bin/env ruby + + require "net/http" + require "json" + require "uri" + require "fileutils" + + CLIENT_ID="YOUR_CLIENT_ID" + + def help + puts "usage: app_cli " + 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("{% data variables.product.oauth_host_code %}/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("{% data variables.product.oauth_host_code %}/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("{% data variables.product.api_url_code %}/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 + ``` + +## Testing + +This tutorial assumes that your app code is stored in a file named `app_cli.rb`. + +1. In your terminal, from the directory where `app_cli.rb` is stored, run `./app_cli.rb help`. You should see output that looks like this. + + ``` + usage: app_cli + ``` + +1. In your terminal, from the directory where `app_cli.rb` is stored, run `./app_cli.rb login`. You should see output that looks like this. The code will differ every time: + + ``` + Please visit: {% data variables.product.oauth_host_code %}/login/device + and enter code: CA86-8D94 + ``` + +1. Navigate to {% data variables.product.oauth_host_code %}/login/device in your browser and enter the code from the previous step, then click **Continue**. +1. {% data variables.product.company_short %} should display a page that prompts you to authorize your app. Click the "Authorize" button. +1. Your terminal should now say "Successfully authenticated!". +1. In your terminal, from the directory where `app_cli.rb` is stored, run `./app_cli.rb whoami`. You should see output that looks like this, where `octocat` is your username. + + ``` + You are octocat + ``` + +1. Open the `.token` file in your editor, and modify the token. Now, the token is invalid. +1. In your terminal, from the directory where `app_cli.rb` is stored, run `./app_cli.rb whoami`. You should see output that looks like this: + + ``` + You are not authorized. Run the `login` command. + ``` + +1. Delete the `.token` file. +1. In your terminal, from the directory where `app_cli.rb` is stored, run `./app_cli.rb whoami`. You should see output that looks like this: + + ``` + You are not authorized. Run the `login` command. + ``` + +## Next steps + +### Adjust the code to meet your app's needs + +This tutorial demonstrated how to write a CLI that uses the device flow to generate a user access token. You can expand this CLI to accept additional commands. For example, you can add a `create-issue` command that opens an issue. Remember to update your app's permissions if your app needs additional permissions for the API requests that you want to make. For more information, see "[AUTOTITLE](/apps/creating-github-apps/creating-github-apps/setting-permissions-for-github-apps)." + +### Securely store tokens + +This tutorial generates a user access token and saves it in a local file. You should never commit this file or publicize the token. + +Depending on your device, you may choose different way to store the token. You should check the best practices for storing tokens on your device. diff --git a/content/apps/creating-github-apps/guides/using-the-web-application-flow-to-generate-a-user-access-token-for-a-github-app.md b/content/apps/creating-github-apps/guides/building-a-login-with-github-button-with-a-github-app.md similarity index 91% rename from content/apps/creating-github-apps/guides/using-the-web-application-flow-to-generate-a-user-access-token-for-a-github-app.md rename to content/apps/creating-github-apps/guides/building-a-login-with-github-button-with-a-github-app.md index 7eebeb2af1..ede121d63b 100644 --- a/content/apps/creating-github-apps/guides/using-the-web-application-flow-to-generate-a-user-access-token-for-a-github-app.md +++ b/content/apps/creating-github-apps/guides/building-a-login-with-github-button-with-a-github-app.md @@ -1,7 +1,7 @@ --- -title: Using the web application flow to generate a user access token for a GitHub App -shortTitle: Web application flow -intro: "Follow this tutorial to write Ruby code to generate a user access token via the web application flow for your {% data variables.product.prodname_github_app %}." +title: Building a "Login with GitHub" button with a GitHub App +shortTitle: Build a "Login" button +intro: 'Follow this tutorial to write Ruby code to generate a user access token via the web application flow for your {% data variables.product.prodname_github_app %}.' versions: fpt: '*' ghes: '*' @@ -9,16 +9,22 @@ versions: ghec: '*' topics: - GitHub Apps +redirect_from: + - /apps/creating-github-apps/guides/using-the-web-application-flow-to-generate-a-user-access-token-for-a-github-app --- ## Introduction -This tutorial demonstrates how to use the web application flow to generate a user access token for a {% data variables.product.prodname_github_app %}. Your app should use a user access token if you want to attribute the app's actions to a user and want to prevent your app from taking over-privileged actions. For more information, see "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app)" and "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-with-a-github-app-on-behalf-of-a-user)." - -If your app does not have access to a web interface, you should use device flow instead. For more information, see "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-device-flow-to-generate-a-user-access-token)." +This tutorial demonstrates how to build a "Login with GitHub" button for a website. The website will use a {% data variables.product.prodname_github_app %} to generate a user access token via the web application flow. Then, the website uses the user access token to make API requests on behalf of the authenticated user. This tutorial uses Ruby, but you can use the web application flow with any programming language that is used for web development. +### About web application flow and user access tokens + +Your app should use a user access token if you want to attribute the app's actions to a user. For more information, see "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-with-a-github-app-on-behalf-of-a-user)." + +There are two ways to generate a user access token for a {% data variables.product.prodname_github_app %}: web application flow and device flow. If your app has access to a web interface, you should use web application flow. If your app does not have access to a web interface, you should use device flow instead. For more information, see "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app)" and "[AUTOTITLE](/apps/creating-github-apps/guides/building-a-cli-with-a-github-app)." + ## Prerequisites This tutorial assumes that you have already created a {% data variables.product.prodname_github_app %}. For more information about creating an app, see "[AUTOTITLE](/apps/creating-github-apps/creating-github-apps/creating-a-github-app)." @@ -409,7 +415,7 @@ Unlike a traditional OAuth token, the user access token does not use scopes so y ### Adjust the code to meet your app's needs -This tutorial demonstrated how to display information about the authenticated user, but you can adjust this code to take other actions. +This tutorial demonstrated how to display information about the authenticated user, but you can adjust this code to take other actions. Remember to update your app's permissions if your app needs additional permissions for the API requests that you want to make. For more information, see "[AUTOTITLE](/apps/creating-github-apps/creating-github-apps/setting-permissions-for-github-apps)." This tutorial stored all of the code into a single file, but you may want to move functions and components into separate files. @@ -418,4 +424,3 @@ This tutorial stored all of the code into a single file, but you may want to mov This tutorial generates a user access token. Unless you opted out of expiration for user access tokens, the user access token will expire after eight hours. You will also receive a refresh token that can regenerate a user access token. For more information, see "[AUTOTITLE](/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens)." If you plan on interacting further with {% data variables.product.company_short %}'s APIs, you should store the token for future use. If you choose to store the user access token or refresh token, you must store it securely. You should never publicize the token. - diff --git a/content/apps/creating-github-apps/guides/index.md b/content/apps/creating-github-apps/guides/index.md index e83397c311..9430dce64a 100644 --- a/content/apps/creating-github-apps/guides/index.md +++ b/content/apps/creating-github-apps/guides/index.md @@ -13,8 +13,10 @@ topics: - GitHub Apps children: - /setting-up-your-development-environment-to-create-a-github-app - - /creating-ci-tests-with-the-checks-api - /using-the-github-api-in-your-app + - /building-a-login-with-github-button-with-a-github-app + - /building-a-cli-with-a-github-app + - /creating-ci-tests-with-the-checks-api - /migrating-oauth-apps-to-github-apps - - /using-the-web-application-flow-to-generate-a-user-access-token-for-a-github-app --- + diff --git a/data/reusables/apps/web-app-flow-token-response.md b/data/reusables/apps/web-app-flow-token-response.md index 0dfcac7550..d8c5c4702e 100644 --- a/data/reusables/apps/web-app-flow-token-response.md +++ b/data/reusables/apps/web-app-flow-token-response.md @@ -1,3 +1,3 @@ -1. {% data variables.product.company_short %} will respond with a response body that includes the following parameters: +1. {% data variables.product.company_short %} will give a response that includes the following parameters: {% data reusables.apps.user-access-token-response-parameters %} \ No newline at end of file