Introduction
This tutorial demonstrates how to build a "Login with GitHub" button for a website. The website will use a 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 Authenticating with a GitHub App on behalf of a user.
There are two ways to generate a user access token for a 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 Generating a user access token for a GitHub App and Building a CLI with a GitHub App.
Prerequisites
This tutorial assumes that you have already registered a GitHub App. For more information about registering a GitHub App, see Registering a GitHub App.
Before following this tutorial, you must set a callback URL for your app. This tutorial uses a local Sinatra server with the default URL of http://localhost:4567. For example, to work with the default URL for a local Sinatra application, your callback URL can be http://localhost:4567/github/callback. Once you are ready to deploy your app, you can change the callback URL to use your live server address. For more information about updating the callback URL for your app, see Modifying a GitHub App registration and About the user authorization callback URL.
This tutorial assumes that you have a basic understanding of Ruby and of the Ruby template system, ERB. For more information, see Ruby and ERB.
Install dependencies
This tutorial uses the Ruby gem, Sinatra, to create a web application with Ruby. For more information, see the Sinatra README.
This tutorial uses the Ruby gem, dotenv, to access values stored in a .env file. For more information, see the dotenv README.
To follow this tutorial, you must install the Sinatra and dotenv gems in your Ruby project. For example, you can do this with Bundler:
-
If you don't already have Bundler installed, run the following command in your terminal:
gem install bundler -
If you don't already have a Gemfile for your app, run the following command in your terminal:
bundle init -
If you don't already have a Gemfile.lock for your app, run the following command in your terminal:
bundle install -
Install the gems by running the following commands in your terminal:
bundle add sinatrabundle add dotenv
Store the client ID and client secret
This tutorial will show you how to store the client ID and client secret in environment variables and access them with ENV.fetch. When you deploy your app, you will want to change how you store the client ID and client secret. For more information, see Securely store your client secret.
-
In the upper-right corner of any page on GitHub, click your profile picture.
-
Navigate to your account settings.
- For an app owned by a personal account, click Settings.
- For an app owned by an organization:
- Click Your organizations.
- To the right of the organization, click Settings.
-
In the left sidebar, click Developer settings.
-
In the left sidebar, click GitHub Apps.
-
Next to the GitHub App that you want to work with, click Edit.
-
On the app's settings page, find the client ID for your app. You will add it to a
.envfile in a following step. Note that the client ID is different from the app ID. -
On the app's settings page, click Generate a new client secret. You will add the client secret to a
.envfile in a following step. -
Create a file called
.envat the same level as yourGemfile. -
If your project doesn't already have a
.gitignorefile, create a.gitignorefile at the same level as yourGemfile. -
Add
.envto your.gitignorefile. This will prevent you from accidentally committing your client secret. For more information about.gitignorefiles, see Ignoring files. -
Add the following contents to your
.envfile. ReplaceYOUR_CLIENT_IDwith the client ID of your app. ReplaceYOUR_CLIENT_SECRETwith the client secret for your app.CLIENT_ID="YOUR_CLIENT_ID" CLIENT_SECRET="YOUR_CLIENT_SECRET"
Add code to generate a user access token
To get a user access token, you first need to prompt the user to authorize your app. When a user authorizes your app, they are redirected to the callback URL for your app. The request to the callback URL includes a code query parameter. When your app gets a request to serve that callback URL, you can exchange the code parameter for a user access token.
These steps lead you through writing code to generate a user access token. To skip ahead to the final code, see Full code example.
-
In the same directory as your
.envfile, create a Ruby file to hold the code that will generate a user access token. This tutorial will name the fileapp.rb. -
At the top of
app.rb, add these dependencies:Ruby require "sinatra" require "dotenv/load" require "net/http" require "json"
require "sinatra" require "dotenv/load" require "net/http" require "json"The
sinatraanddotenv/loaddependencies use the gems that you installed earlier.net/httpandjsonare part of the Ruby standard library. -
Add the following code to
app.rb, to get your app's client ID and client secret from your.envfile.Ruby CLIENT_ID = ENV.fetch("CLIENT_ID") CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")CLIENT_ID = ENV.fetch("CLIENT_ID") CLIENT_SECRET = ENV.fetch("CLIENT_SECRET") -
Add the following code to
app.rbto display a link that will prompt users to authenticate your app.Ruby get "/" do link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end
get "/" do link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end -
Add the following code to
app.rbto handle requests to your app's callback URL and get thecodeparameter from the request. ReplaceCALLBACK_URLwith the callback URL for your app, minus the domain. For example, if your callback URL ishttp://localhost/github/callback, replaceCALLBACK_URLwith/github/callback.Ruby get "CALLBACK_URL" do code = params["code"] render = "Successfully authorized! Got code #{code}." erb render endget "CALLBACK_URL" do code = params["code"] render = "Successfully authorized! Got code #{code}." erb render endCurrently, the code just renders a message along with the
codeparameter. The following steps will expand this code block. -
Optionally, check your progress:
app.rbnow looks like this, whereCALLBACK_URLis the callback URL for your app, minus the domain:Ruby require "sinatra" require "dotenv/load" require "net/http" require "json" CLIENT_ID = ENV.fetch("CLIENT_ID") CLIENT_SECRET = ENV.fetch("CLIENT_SECRET") get "/" do link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end get "CALLBACK_URL" do code = params["code"] render = "Successfully authorized! Got code #{code}." erb render endrequire "sinatra" require "dotenv/load" require "net/http" require "json" CLIENT_ID = ENV.fetch("CLIENT_ID") CLIENT_SECRET = ENV.fetch("CLIENT_SECRET") get "/" do link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end get "CALLBACK_URL" do code = params["code"] render = "Successfully authorized! Got code #{code}." erb render end-
In your terminal, from the directory where
app.rbis stored, runruby app.rb. A local Sinatra server should start. -
In your browser, navigate to
http://localhost:4567. You should see a link with the text "Login with GitHub". -
Click on the "Login with GitHub" link.
If you have not authorized the app, clicking on the link should take you to
https://github.com/login/oauth/authorize?client_id=CLIENT_ID, whereCLIENT_IDis the client ID of your app. This is a GitHub page that prompts users to authorize your app. If you click the button to authorize your app, you will go to the callback URL for your app.If you previously authorized your app and the authorization has not been revoked, you will skip the authorization prompt and go directly to the callback URL instead. You can revoke your previous authorization if you want to see the authorization prompt. For more information, see Reviewing and revoking authorization of GitHub Apps.
-
The callback URL page, reached by clicking the "Login with GitHub" link and then authorizing the app if prompted to do so, should display the text similar to "Successfully authorized! Got code agc622abb6135be5d1f2."
-
In your terminal where Sinatra is running, stop the server by entering Ctrl+C.
-
-
Replace the content of
app.rbwith the following code, whereCALLBACK_URLis the callback URL for your app, minus the domain.This code adds logic to exchange the
codeparameter for a user access token:- The
parse_responsefunction parses the response from the GitHub API. - The
exchange_codefunction exchanges thecodeparameter for a user access token. - The handler for the callback URL request now calls
exchange_codeto exchange the code parameter for a user access token. - The callback page now shows text to indicate that a token was generated. If the token generation was not successful, the page will indicate that failure.
Ruby require "sinatra" require "dotenv/load" require "net/http" require "json" CLIENT_ID = ENV.fetch("CLIENT_ID") CLIENT_SECRET = ENV.fetch("CLIENT_SECRET") def parse_response(response) case response when Net::HTTPOK JSON.parse(response.body) else puts response puts response.body {} end end def exchange_code(code) params = { "client_id" => CLIENT_ID, "client_secret" => CLIENT_SECRET, "code" => code } result = Net::HTTP.post( URI("https://github.com/login/oauth/access_token"), URI.encode_www_form(params), {"Accept" => "application/json"} ) parse_response(result) end get "/" do link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end get "CALLBACK_URL" do code = params["code"] token_data = exchange_code(code) if token_data.key?("access_token") token = token_data["access_token"] render = "Successfully authorized! Got code #{code} and exchanged it for a user access token ending in #{token[-9..-1]}." erb render else render = "Authorized, but unable to exchange code #{code} for token." erb render end end - The