このセクションでは、認証の基本に焦点を当てます。 具体的には、アプリケーションのウェブフローを実装した、(Sinatra を使う) Rubyサーバーを、いくつかの方法で作成します。
このプロジェクトの完全なソースコードは、platform-samples リポジトリからダウンロードできます。
アプリケーションの登録
まず、アプリケーションの登録が必要です。 登録された各 OAuth アプリケーションには、一意のクライアント ID とクライアントシークレットが割り当てられます。 クライアントシークレットは共有しないでく� さい。 共有には、文字列をリポジトリにチェックインすることも含まれます。
どのような情� �を入力しても構いませんが、認証コールバック URL は例外です。 これが、アプリケーションの設定にあたってもっとも重要な情� �と言えるでしょう。 認証の成功後に GitHub Enterprise Server がユーザに返すのは、コールバックURLなのです。
通常の Sinatra サーバーを実行しているので、ローカルインスタンスの� �所は http://127.0.0.1:4567
に設定されています。 コールバック URL を http://127.0.0.1:4567/callback
と入力しましょう。
ユーザ認証の承認
非推奨の注意: GitHubは、クエリパラメータを使ったAPIの認証を廃止します。 APIの認証はHTTPの基本認証で行わなければなりません。予定された一時停止を含む詳しい情� �についてはブログポストを参照してく� さい。
クエリパラメータを使ったAPIの認証は、利用はできるものの、セキュリティ上の懸念からサポートされなくなりました。 その代わりに、インテグレータはアクセストークン、client_id
もしくはclient_secret
をヘッダに移すことをおすすめします。 GitHubは、クエリパラメータによる認証の削除を、事前に通知します。
さて、簡単なサーバーの入力を始めましょう。 server.rb というファイルを作成し、以下の内容を貼り付けてく� さい。
require 'sinatra'
require 'rest-client'
require 'json'
CLIENT_ID = ENV['GH_BASIC_CLIENT_ID']
CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID']
get '/' do
erb :index, :locals => {:client_id => CLIENT_ID}
end
クライアント ID とクライアントシークレットは、アプリケーションの設定ページから取得されます。 これらは 環境変数として保存することをお勧めします。この例でも、そのようにしています。
次に、views/index.erbに以下の内容を貼り付けてく� さい。
<html>
<head>
</head>
<body>
<p>
Well, hello there!
</p>
<p>
We're going to now talk to the GitHub API. Ready?
<a href="https://github.com/login/oauth/authorize?scope=user:email&client_id=<%= client_id %>">Click here</a> to begin!
</p>
<p>
If that link doesn't work, remember to provide your own <a href="/apps/building-oauth-apps/authorizing-oauth-apps/">Client ID</a>!
</p>
</body>
</html>
(Sinatraの仕組みに詳しくない方は、Sinatraのガイドを読むことをお勧めします。)
URLはアプリケーションに要求されたスコープをscope
クエリパラメータで定義していることにも注目しましょう。 このアプリケーションでは、プライベートのメールアドレスを読み込むため、user:email
スコープをリクエストしています。
ブラウザでhttp://127.0.0.1:4567
にアクセスしてく� さい。 リンクをクリックすると、GitHub Enterprise Serverに移動し、以下のようなダイアログが表示されます。
あなた自身を信用する� �合は、[Authorize App]をクリックします。 おっと、 Sinatraが404
エラーを吐き出しました。 いったい何が起こったのでしょうか。
さて、コールバックURLをcallback
に指定したときのことを覚えていますか。 そのときルートを設定しなかったので、GitHub Enterprise Serverはアプリケーションを認証した後、ユーザをどこにドロップするかがわからなかったのです。 では、この問題を解決しましょう。
コールバックの設定
server.rbにルートを追� して、コールバックが実行すべきことを指定します。
get '/callback' do
# get temporary GitHub code...
session_code = request.env['rack.request.query_hash']['code']
# ... and POST it back to GitHub
result = RestClient.post('https://github.com/login/oauth/access_token',
{:client_id => CLIENT_ID,
:client_secret => CLIENT_SECRET,
:code => session_code},
:accept => :json)
# extract the token and granted scopes
access_token = JSON.parse(result)['access_token']
end
アプリケーションの認証に成功すると、GitHub Enterprise Serverは一時的なcode
値を提供します。 このコードを、access_token
と引き換えに、POST
でGitHub Enterprise Serverに戻す必要があります。 GETおよびPOSTのHTTPリクエストをを簡� 化するために、 rest-clientを使用しています。 REST経由でAPIにアクセスすることは、おそらくないということに留意してく� さい。 もっと本� �的なアプリケーションであれば、お好みの言語で書かれたライブラリを使った方がいいでしょう。
付与されたスコープの確認
URL を直接変更すれば、ユーザはリクエストしたスコープを編集できます。 こうすると、アプリケーションに対して元々リクエストしたよりも少ないアクセス� けを許可できます。 トークンでリクエストを行う前に、ユーザからトークンに付与されたスコープを確認してく� さい。 詳しい情� �については、「OAuth App のスコープ」を参照してく� さい。
付与されたスコープは、トークンの交換によるレスポンスの一部として返されます。
get '/callback' do
# ...
# Get the access_token using the code sample above
# ...
# check if we were granted user:email scope
scopes = JSON.parse(result)['scope'].split(',')
has_user_email_scope = scopes.include? 'user:email'
end
このアプリケーションでは、認証されたユーザのプライベートメールアドレスをフェッチするために必要なuser:email
スコープが付与されたかを確認するためscopes.include?
を使用しています。 アプリケーションが他のスコープを要求していた� �合は、それも確認します。
また、スコープ間には階層的な関係があるため、必要な最低限のスコープが付与されたか確認する必要があります。 たとえば、アプリケーションが user
スコープを要求していた� �合、user:email
スコープしか付与されていないかもしれません。 この� �合、アプリケーションが要求したスコープは付与されていないかもしれませんが、付与されたスコープで十分� ったでしょう。
リクエストを行う前にのみスコープを確認する� けでは不十分です。確認時と実際のリクエスト時の間に、ユーザがスコープを変更する可能性があります。 このような� �合には、成功すると思っていたAPIの呼び出しが404
または401
ステータスになって失敗したり、情� �の別のサブセットを返したりします。
この状況にうまく対応できるように、有効なトークンによるリクエストに対するすべてのAPIレスポンスには、X-OAuth-Scopes
ヘッダも含まれています。 このヘッダには、リクエストを行うために使用されたトークンのスコープのリストが含まれています。 それに� えて、OAuthアプリケーションAPIは、トークンの有効性のチェックのためのエンドポイントを提供します。 この情� �を使用してトークンのスコープにおける変更を検出し、利用可能なアプリケーション機能の変更をユーザに通知します。
認証リクエストの実施
最後に、このアクセストークンで、ログインしたユーザとして認証のリクエストを行うことができます。
# fetch user information
auth_result = JSON.parse(RestClient.get('http(s)://[hostname]/api/v3/user',
{:params => {:access_token => access_token}}))
# if the user authorized it, fetch private emails
if has_user_email_scope
auth_result['private_emails'] =
JSON.parse(RestClient.get('http(s)://[hostname]/api/v3/user/emails',
{:params => {:access_token => access_token}}))
end
erb :basic, :locals => auth_result
この結果を使って、やりたいことができます。 この例では、それらを単純にbasic.erbに直接書き出します。
<p>Hello, <%= login %>!</p>
<p>
<% if !email.nil? && !email.empty? %> It looks like your public email address is <%= email %>.
<% else %> It looks like you don't have a public email. That's cool.
<% end %>
</p>
<p>
<% if defined? private_emails %>
With your permission, we were also able to dig up your private email addresses:
<%= private_emails.map{ |private_email_address| private_email_address["email"] }.join(', ') %>
<% else %>
Also, you're a bit secretive about your private email addresses.
<% end %>
</p>
「永続的な」認証の実装
ウェブページにアクセスするたびに、ユーザにアプリケーションへのログインを求めるというのは非常に悪いモデルです。 たとえば、http://127.0.0.1:4567/basic
に直接アクセスしてみてく� さい。 エラーになるでしょう。
「ここをクリック」というプロセスをすべてなくし、ユーザが_ GitHub Enterprise Server にログインしている限りそれを記憶して、このアプリケーションにアクセスできるとしたらどうでしょうか。 実のところ、 これからやろうとしていること_はまさにそういうことなのです。
上記に上げたサーバはかなり単純なものです。 インテリジェントな認証を入れるために、トークンを保存するためセッションを使用するよう切り替えます。 これにより、認証はユーザーに意識されないものになります。
また、セッション内のスコープを永続的にしているため、そのスコープを確認した後にユーザが更新した� �合や、トークンを取り消した� �合に対処する必要があります。 これを行うために、rescue
ブロックを使用し、最初のAPI呼び出しが成功したことを確認し、トークンがま� 有効であることを確かめます。 次に、X-OAuth-Scopes
レスポンスヘッダで、ユーザがuser:email
スコープを取り消していないことを確かめます。
advanced_server.rbというファイルを作成し、以下の行を貼り付けてく� さい。
require 'sinatra'
require 'rest_client'
require 'json'
# !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!!
# Instead, set and test environment variables, like below
# if ENV['GITHUB_CLIENT_ID'] && ENV['GITHUB_CLIENT_SECRET']
# CLIENT_ID = ENV['GITHUB_CLIENT_ID']
# CLIENT_SECRET = ENV['GITHUB_CLIENT_SECRET']
# end
CLIENT_ID = ENV['GH_BASIC_CLIENT_ID']
CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID']
use Rack::Session::Pool, :cookie_only => false
def authenticated?
session[:access_token]
end
def authenticate!
erb :index, :locals => {:client_id => CLIENT_ID}
end
get '/' do
if !authenticated?
authenticate!
else
access_token = session[:access_token]
scopes = []
begin
auth_result = RestClient.get('http(s)://[hostname]/api/v3/user',
{:params => {:access_token => access_token},
:accept => :json})
rescue => e
# request didn't succeed because the token was revoked so we
# invalidate the token stored in the session and render the
# index page so that the user can start the OAuth flow again
session[:access_token] = nil
return authenticate!
end
# the request succeeded, so we check the list of current scopes
if auth_result.headers.include? :x_oauth_scopes
scopes = auth_result.headers[:x_oauth_scopes].split(', ')
end
auth_result = JSON.parse(auth_result)
if scopes.include? 'user:email'
auth_result['private_emails'] =
JSON.parse(RestClient.get('http(s)://[hostname]/api/v3/user/emails',
{:params => {:access_token => access_token},
:accept => :json}))
end
erb :advanced, :locals => auth_result
end
end
get '/callback' do
session_code = request.env['rack.request.query_hash']['code']
result = RestClient.post('https://github.com/login/oauth/access_token',
{:client_id => CLIENT_ID,
:client_secret => CLIENT_SECRET,
:code => session_code},
:accept => :json)
session[:access_token] = JSON.parse(result)['access_token']
redirect '/'
end
コードの大部分は見慣れたもののはずです。 たとえば、ここでもGitHub Enterprise Server APIを呼び出すためにRestClient.get
を使用し、 またERBテンプレート (この例ではadvanced.erb
) に結果をレンダリングするため結果を渡しています。
また、ここではauthenticated?
メソッドを使い、ユーザがすでに認証されているかを確認しています。 認証されていない� �合は、authenticate!
メソッドが呼び出され、OAuthのフローを実行して、付与されたトークンとスコープでセッションを更新します。
次に、 views内にadvanced.erbというファイルを作成し、以下のマークアップを貼り付けてく� さい。
<html>
<head>
</head>
<body>
<p>Well, well, well, <%= login %>!</p>
<p>
<% if !email.empty? %> It looks like your public email address is <%= email %>.
<% else %> It looks like you don't have a public email. That's cool.
<% end %>
</p>
<p>
<% if defined? private_emails %>
With your permission, we were also able to dig up your private email addresses:
<%= private_emails.map{ |private_email_address| private_email_address["email"] }.join(', ') %>
<% else %>
Also, you're a bit secretive about your private email addresses.
<% end %>
</p>
</body>
</html>
コマンドラインからruby advanced_server.rb
を呼び出します。このコマンドは、ポート4567
(単純なSinatraアプリケーションを使用していた時と同じポート) でサーバーを起動します。 http://127.0.0.1:4567
にアクセスすると、アプリケーションはauthenticate!
を呼び出し、/callback
にリダイレクトします。 そして/callback
で/
に戻され、認証が終わっているのでadvanced.erbがレンダリングされます。
GitHub Enterprise ServerのコールバックURLを/
にする� けで、このラウンドトリップ経路を単純化できました。 た� し、server.rbとadvanced.rbの両方が同じコールバックURLに依存しているため、動作は少し不安定になります。
また、このアプリケーションをGitHub Enterprise Serverデータにアクセスするよう認証したことがない� �合、以前と同じ確認ダイアログが表示され、警告されるでしょう。