はじめに
このチュートリアルでは、Web サイトの [GitHub でログイン] ボタンを作成する方法について説明します。 Web サイトでは、GitHub App を使って、Web アプリケーション フローを介してユーザー アクセス トークンを生成します。 その後、Web サイトではそのユーザー アクセス トークンを使って、認証されたユーザーの代わりに API 要求を行います。
このチュートリアルでは Ruby を使いますが、Web 開発に使われる任意のプログラミング言語で Web アプリケーション フローを使うことができます。
Web アプリケーション フローとユーザー アクセス トークンについて
アプリのアクションをユーザーの属性にする場合は、アプリでユーザー アクセス トークンを使う必要があります。 詳しくは、「ユーザーに代わって GitHub アプリで認証する」を参照してください。
GitHub App 用のユーザー アクセス トークンを生成するには、Web アプリケーション フローとデバイス フローの 2 つの方法があります。 アプリが Web インターフェイスにアクセスできる場合は、Web アプリケーション フローを使う必要があります。 アプリから Web インターフェイスにアクセスできない場合は、代わりにデバイス フローを使う必要があります。 詳細については、「GitHub アプリのユーザー アクセス トークンの生成」および「GitHub アプリを使用して CLI を構築する」を参照してください。
前提条件
このチュートリアルでは、GitHub App を既に登録済みであることを前提としています。 GitHub App の登録について詳しくは、「GitHub App の登録」を参照してください。
このチュートリアルに従う前に、アプリのコールバック URL を設定する必要があります。 このチュートリアルでは、既定の URL が http://localhost:4567
のローカル Sinatra サーバーを使います。 たとえば、ローカル Sinatra アプリケーションの既定の URL で動作するように、コールバック URL を http://localhost:4567/github/callback
にできます。 アプリをデプロイする準備ができたら、コールバック URL をライブ サーバー アドレスを使うように変更できます。 アプリのコールバック URL の更新について詳しくは、「GitHub App 登録の変更」と「ユーザー承認コールバック URL について」を参照してください。
このチュートリアルでは、Ruby と Ruby テンプレート システム ERB の基本的な知識があることを前提としています。 詳しくは、Ruby と ERB を参照してください。
依存関係のインストール
このチュートリアルでは、Ruby の gem である Sinatra を使って、Ruby で Web アプリケーションを作成します。 詳しくは、Sinatra の README をご覧ください。
このチュートリアルでは、Ruby の gem である dotenv を使って、.env
ファイルに保存されている値にアクセスします。 詳しくは、dotenv の README をご覧ください。
このチュートリアルに従うには、Ruby プロジェクトに Sinatra と dotenv の gem をインストールする必要があります。 たとえば、これは Bundler を使って行うことができます。
-
Bundler がまだインストールされていない場合は、ターミナルで次のコマンドを実行します。
gem install bundler
-
アプリに Gemfile がまだない場合は、ターミナルで次のコマンドを実行します。
bundle init
-
アプリに Gemfile.lock がまだない場合は、ターミナルで次のコマンドを実行します。
bundle install
-
ターミナルで次のコマンドを実行して gem をインストールします。
bundle add sinatra
bundle add dotenv
クライアント ID とクライアント シークレットを保存する
このチュートリアルでは、クライアント ID とクライアント シークレットを環境変数に保存し、ENV.fetch
を使ってそれらにアクセスする方法について説明します。 アプリをデプロイするときに、クライアント ID とクライアント シークレットの保存方法を変更することをお勧めします。 詳しくは、「クライアント シークレットを安全に保存する」を参照してください。
-
GitHub の任意のページの右上隅にある、自分のプロファイル写真をクリックします。
-
アカウント設定にアクセスしてください。
- 個人用アカウントが所有するアプリの場合は、[設定] をクリックします。
- 組織が所有するアプリの場合:
- [自分の組織] をクリックします。
- 組織の右側にある [設定] をクリックします。
-
左側のサイドバーで [ 開発者設定] をクリックします。
-
左側のサイドバーで、 [GitHub Apps] をクリックします。
-
作業したい GitHub App の横にある [編集] を選びます。
-
アプリの設定ページで、ご自分のアプリのクライアント ID を見つけます。 これは、次の手順で
.env
ファイルに追加します。 クライアント ID は、アプリ ID とは異なることに注意してください。 -
アプリの設定ページで、 [新しいクライアント シークレットの生成] をクリックします。 クライアント シークレットは、次の手順で
.env
ファイルに追加します。 -
.env
というファイルをGemfile
と同じレベルに作成します。 -
プロジェクトに
.gitignore
ファイルがまだない場合は、.gitignore
ファイルをGemfile
と同じレベルに作成します。 -
.gitignore
ファイルに.env
を追加します。 こうすることで、クライアント シークレットを誤ってコミットするのを防ぐことができます。.gitignore
ファイルについて詳しくは、「ファイルを無視する」を参照してください。 -
次の内容を
.env
ファイルに追加します。YOUR_CLIENT_ID
は、ご自分のアプリのクライアント ID に置き換えます。YOUR_CLIENT_SECRET
は、ご自分のアプリのクライアント シークレットに置き換えます。CLIENT_ID="YOUR_CLIENT_ID" CLIENT_SECRET="YOUR_CLIENT_SECRET"
ユーザー アクセス トークンを生成するコードを追加する
ユーザー アクセス トークンを取得するには、まずユーザーにアプリの認可を求める必要があります。 ユーザーがアプリを認可すると、アプリのコールバック URL にリダイレクトされます。 コールバック URL への要求に code
クエリ パラメーターを含めます。 アプリでそのコールバック URL を提供する要求を受け取ると、code
パラメーターをユーザー アクセス トークンと交換できます。
次の手順に従って、ユーザー アクセス トークンを生成するコードを記述します。 スキップして最終的なコードに進むには、「完全なコード例」を参照してください。
-
.env
ファイルと同じディレクトリに Ruby ファイルを作成して、ユーザー アクセス トークンを生成するコードを保持します。 このチュートリアルでは、ファイルにapp.rb
という名前を付けます。 -
app.rb
の先頭に、次の依存関係を追加します。Ruby require "sinatra" require "dotenv/load" require "net/http" require "json"
require "sinatra" require "dotenv/load" require "net/http" require "json"
sinatra
依存関係とdotenv/load
依存関係で、前にインストールした gem を使います。net/http
とjson
は Ruby 標準ライブラリに含まれています。 -
次のコードを
app.rb
に追加して、.env
ファイルからアプリのクライアント ID とクライアント シークレットを取得します。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")
-
次のコードを
app.rb
に追加して、ユーザーにアプリの認証を求めるリンクを表示します。Ruby get "/" do link = '<a href="http(s)://HOSTNAME/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end
get "/" do link = '<a href="http(s)://HOSTNAME/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end
-
次のコードを
app.rb
に追加して、アプリのコールバック URL への要求を処理し、要求からcode
パラメーターを取得します。CALLBACK_URL
を、アプリのコールバック URL からドメインを除いた値に置き換えます。 たとえば、コールバック URL がhttp://localhost:4567/github/callback
の場合は、CALLBACK_URL
を/github/callback
に置き換えます。Ruby get "CALLBACK_URL" do code = params["code"] render = "Successfully authorized! Got code #{code}." erb render end
get "CALLBACK_URL" do code = params["code"] render = "Successfully authorized! Got code #{code}." erb render end
現段階でこのコードによってレンダリングされるのは、メッセージと
code
パラメーターだけです。 次の手順で、このコード ブロックを拡張します。 -
必要に応じて、進行状況をチェックします。
app.rb
は次のようになります。CALLBACK_URL
はアプリのコールバック URL からドメインを除いた値です。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="http(s)://HOSTNAME/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
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="http(s)://HOSTNAME/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
-
ターミナルで、
app.rb
が保存されているディレクトリから、ruby app.rb
を実行します。 ローカルの Sinatra サーバーが起動するはずです。 -
ブラウザーで
http://localhost:4567
にアクセスします。 "Login with GitHub" というテキストのリンクが表示されるはずです。 -
"Login with GitHub" のリンクをクリックします。
アプリを認可していない場合は、リンクをクリックすると
http(s)://HOSTNAME/login/oauth/authorize?client_id=CLIENT_ID
に移動します。CLIENT_ID
はアプリのクライアント ID です。 これは、ユーザーにアプリの認可を求める GitHub ページです。 ボタンをクリックしてアプリを認可すると、アプリのコールバック URL に移動します。以前にアプリを認可し、認可が取り消されていない場合は、認可プロンプトをスキップし、代わりにコールバック URL に直接移動します。 認可ダイアログを表示したい場合は、以前の認可を取り消すことができます。 詳しくは、「GitHub Apps の承認の確認と取り消し」を参照してください。
-
"Login with GitHub" リンクをクリックし、アプリを認可するように求められたら、そうすることで表示されるコールバック URL ページに、次のようなテキストが表示されます: "Successfully authorized! Got code agc622abb6135be5d1f2."
-
Sinatra が実行されているターミナルで、Ctrl+C キーを押してサーバーを停止します。
-
-
app.rb
の内容を次のコードに置き換えます。CALLBACK_URL
は、アプリのコールバック URL からドメインを除いた値です。このコードでは、
code
パラメーターをユーザー アクセス トークンと交換するロジックを追加します。parse_response
関数で、GitHub API からの応答を解析します。exchange_code
関数で、code
パラメーターをユーザー アクセス トークンと交換します。- コールバック URL 要求のハンドラーで
exchange_code
を呼び出して、code パラメーターをユーザー アクセス トークンと交換するようになります。 - コールバック ページに、トークンが生成されたことを示すテキストが表示されるようになります。 トークン生成が成功しなかった場合は、ページにそのエラーが示されます。
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("http(s)://HOSTNAME/login/oauth/access_token"), URI.encode_www_form(params), {"Accept" => "application/json"} ) parse_response(result) end get "/" do link = '<a href="http(s)://HOSTNAME/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
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("http(s)://HOSTNAME/login/oauth/access_token"), URI.encode_www_form(params), {"Accept" => "application/json"} ) parse_response(result) end get "/" do link = '<a href="http(s)://HOSTNAME/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
-
必要に応じて、進行状況をチェックします。
- ターミナルで、
app.rb
が保存されているディレクトリから、ruby app.rb
を実行します。 ローカルの Sinatra サーバーが起動するはずです。 - ブラウザーで
http://localhost:4567
にアクセスします。 "Login with GitHub" というテキストのリンクが表示されるはずです。 - "Login with GitHub" のリンクをクリックします。
- アプリを認可するように求められたら、そうします。
- "Login with GitHub" リンクをクリックし、アプリを認可するように求められたら、そうすることで表示されるコールバック URL ページに、次のようなテキストが表示されます: "Successfully authorized! Got code 4acd44861aeda86dacce and exchanged it for a user access token ending in 2zU5kQziE."
- Sinatra が実行されているターミナルで、Ctrl+C キーを押してサーバーを停止します。
- ターミナルで、
-
これでユーザー アクセス トークンを取得したので、このトークンを使って、ユーザーに代わって API 要求を行うことができます。 次に例を示します。
この関数を
app.rb
に追加して、/user
REST API エンドポイントでユーザーに関する情報を取得します。Ruby def user_info(token) uri = URI("http(s)://HOSTNAME/api/v3/user") result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| body = {"access_token" => token}.to_json auth = "Bearer #{token}" headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth} http.send_request("GET", uri.path, body, headers) end parse_response(result) end
def user_info(token) uri = URI("http(s)://HOSTNAME/api/v3/user") result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| body = {"access_token" => token}.to_json auth = "Bearer #{token}" headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth} http.send_request("GET", uri.path, body, headers) end parse_response(result) end
user_info
関数を呼び出し、ユーザーの名前と GitHub ログインを表示するようにコールバック ハンドラーを更新します。CALLBACK_URL
を、アプリのコールバック URL からドメインを除いた値に置き換えることを忘れないでください。Ruby get "CALLBACK_URL" do code = params["code"] token_data = exchange_code(code) if token_data.key?("access_token") token = token_data["access_token"] user_info = user_info(token) handle = user_info["login"] name = user_info["name"] render = "Successfully authorized! Welcome, #{name} (#{handle})." erb render else render = "Authorized, but unable to exchange code #{code} for token." erb render end end
get "CALLBACK_URL" do code = params["code"] token_data = exchange_code(code) if token_data.key?("access_token") token = token_data["access_token"] user_info = user_info(token) handle = user_info["login"] name = user_info["name"] render = "Successfully authorized! Welcome, #{name} (#{handle})." erb render else render = "Authorized, but unable to exchange code #{code} for token." erb render end end
-
コードを次のセクションの完全なコード例に照らしてチェックします。 コードをテストするには、完全なコード例の後の「テスト」セクションで説明されている手順に従います。
完全なコード例
次に、前のセクションで概要を説明したコードの完全な例を示します。
CALLBACK_URL
を、アプリのコールバック URL からドメインを除いた値に置き換えます。 たとえば、コールバック URL が http://localhost:4567/github/callback
の場合は、CALLBACK_URL
を /github/callback
に置き換えます。
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("http(s)://HOSTNAME/login/oauth/access_token"), URI.encode_www_form(params), {"Accept" => "application/json"} ) parse_response(result) end def user_info(token) uri = URI("http(s)://HOSTNAME/api/v3/user") result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| body = {"access_token" => token}.to_json auth = "Bearer #{token}" headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth} http.send_request("GET", uri.path, body, headers) end parse_response(result) end get "/" do link = '<a href="http(s)://HOSTNAME/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"] user_info = user_info(token) handle = user_info["login"] name = user_info["name"] render = "Successfully authorized! Welcome, #{name} (#{handle})." erb render else render = "Authorized, but unable to exchange code #{code} for token." erb render end end
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("http(s)://HOSTNAME/login/oauth/access_token"),
URI.encode_www_form(params),
{"Accept" => "application/json"}
)
parse_response(result)
end
def user_info(token)
uri = URI("http(s)://HOSTNAME/api/v3/user")
result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
body = {"access_token" => token}.to_json
auth = "Bearer #{token}"
headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth}
http.send_request("GET", uri.path, body, headers)
end
parse_response(result)
end
get "/" do
link = '<a href="http(s)://HOSTNAME/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"]
user_info = user_info(token)
handle = user_info["login"]
name = user_info["name"]
render = "Successfully authorized! Welcome, #{name} (#{handle})."
erb render
else
render = "Authorized, but unable to exchange code #{code} for token."
erb render
end
end
テスト
このチュートリアルでは、アプリ コードが app.rb
という名前のファイルに保存されていること、ローカル Sinatra アプリケーションの既定の URL http://localhost:4567
を使っていることを前提としています。
-
ターミナルで、
app.rb
が保存されているディレクトリから、ruby app.rb
を実行します。 ローカルの Sinatra サーバーが起動するはずです。 -
ブラウザーで
http://localhost:4567
にアクセスします。 "Login with GitHub" というテキストのリンクが表示されるはずです。 -
"Login with GitHub" のリンクをクリックします。
アプリを認可していない場合は、リンクをクリックすると
http(s)://HOSTNAME/login/oauth/authorize?client_id=CLIENT_ID
に移動します。CLIENT_ID
はアプリのクライアント ID です。 これは、ユーザーにアプリの認可を求める GitHub ページです。 ボタンをクリックしてアプリを認可すると、アプリのコールバック URL に移動します。以前にアプリを認可し、認可が取り消されていない場合は、認可プロンプトをスキップし、代わりにコールバック URL に直接移動します。 認可ダイアログを表示したい場合は、以前の認可を取り消すことができます。 詳しくは、「GitHub Apps の承認の確認と取り消し」を参照してください。
-
"Login with GitHub" リンクをクリックし、アプリを認可するように求められたら、そうすることで表示されるコールバック URL ページに、次のようなテキストが表示されます: "Successfully authorized! Welcome, Mona Lisa (octocat)."
-
Sinatra が実行されているターミナルで、Ctrl+C キーを押してサーバーを停止します。
次の手順
クライアント シークレットを安全に保存する
アプリのクライアント シークレットは決して公開しないでください。 このチュートリアルでは、クライアント シークレットを gitignore の .env
ファイルに保存し、ENV.fetch
を使って値にアクセスしました。 アプリをデプロイする際に、クライアント シークレットを保存する安全な方法を選び、それに応じて値を取得するようにコードを更新する必要があります。
たとえば、アプリケーションをデプロイするサーバー上の環境変数にシークレットを保存できます。 Azure Key Vault などのシークレット管理サービスを使うこともできます。
デプロイ用にコールバック URL を更新する
このチュートリアルでは、http://localhost:4567
で始まるコールバック URL を使いました。 ただし、Sinatra サーバーを起動する場合は、http://localhost:4567
はお使いのコンピューターでローカルにのみ使うことができます。 アプリをデプロイする前に、運用環境で使うコールバック URL を使うようにコールバック URL を更新する必要があります。 アプリのコールバック URL の更新について詳しくは、「GitHub App 登録の変更」と「ユーザー承認コールバック URL について」を参照してください。
複数のコールバック URL を処理する
このチュートリアルでは、1 つのコールバック URL を使いましたが、アプリには最大 10 個のコールバック URL を含めることができます。 複数のコールバック URL を使う場合は、次のようにします。
- 追加のコールバック URL をアプリに追加します。 コールバック URL の追加について詳しくは、「GitHub App 登録の変更」を参照してください。
http(s)://HOSTNAME/login/oauth/authorize
にリンクする場合は、redirect_uri
クエリ パラメーターを使って、ユーザーを目的のコールバック URL にリダイレクトします。 詳しくは、「GitHub アプリのユーザー アクセス トークンの生成」を参照してください。- アプリのコードで、
get "CALLBACK_URL" do
で始まるコード ブロックと同じように各コールバック URL を処理します。
追加のパラメーターの指定
http(s)://HOSTNAME/login/oauth/authorize
にリンクする際、追加のクエリ パラメーターを渡すことができます。 詳しくは、「GitHub アプリのユーザー アクセス トークンの生成」を参照してください。
従来の OAuth トークンとは異なり、ユーザー アクセス トークンではスコープを使わないため、scope
パラメーターを使ってスコープを指定することはできません。 代わりに、きめ細かいアクセス許可が使用されます。 ユーザー アクセス トークンを使用すると、ユーザーとアプリの両方が持っているアクセス許可のみが付与されます。
アプリのニーズに合わせてコードを調整する
このチュートリアルでは、認証されたユーザーに関する情報を表示する方法を示しましたが、このコードを調整して他のアクションを実行することもできます。 行う API 要求でアプリに追加のアクセス許可が必要な場合は、忘れずにアプリのアクセス許可を更新してください。 詳しくは、「GitHub アプリのアクセス許可を選択する」を参照してください。
このチュートリアルでは、すべてのコードを 1 つのファイルに保存しましたが、関数とコンポーネントを別のファイルに移動することもできます。
トークンを安全に保存する
このチュートリアルでは、ユーザー アクセス トークンを生成します。 ユーザー アクセス トークンの有効期限をオプトアウトしない限り、ユーザー アクセス トークンは 8 時間後に期限切れになります。 ユーザー アクセス トークンを再生成できる更新トークンも受け取ります。 詳しくは、「ユーザー アクセス トークンを更新する」を参照してください。
GitHub の API とさらにやり取りする予定がある場合は、将来使用できるようにトークンを保存しておく必要があります。 ユーザー アクセス トークンまたは更新トークンを保存する場合は、安全に保存する必要があります。 トークンは決して公開しないでください。
詳しくは、「GitHub App を作成するためのベスト プラクティス」を参照してください。
ベスト プラクティスに従う
GitHub App に関するベスト プラクティスに従うようにする必要があります。 詳しくは、「GitHub App を作成するためのベスト プラクティス」を参照してください。