このセクションでは、認証の基本に焦点を当てます。 具体的には、アプリケーションのウェブフローを実装した、(Sinatra を使う) Rubyサーバーを、いくつかの方法で作成します。
このプロジェクトの完全なソースコードは、platform-samples リポジトリからダウンロードできます。
アプリケーションの登録
まず、アプリケーションの登録が必要です。 登録された各 OAuth アプリケーションには、一意のクライアント ID とクライアントシークレットが割り当てられます。 クライアントシークレットは共有しないでく� さい。 共有には、文字列をリポジトリにチェックインすることも含まれます。
どのような情� �を入力しても構いませんが、認証コールバック URL は例外です。 これが、アプリケーションの設定にあたってもっとも重要な情� �と言えるでしょう。 認証の成功後に GitHub Enterprise Server がユーザに返すのは、コールバックURLなのです。
通常の Sinatra サーバーを実行しているので、ローカルインスタンスの� �所は http://localhost:4567
に設定されています。 コールバック URL を http://localhost: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のガイドを読むことをお勧めします。)
URLはアプリケーションに要求されたスコープをscope
クエリパラメータで定義していることにも注目しましょう。 このアプリケーションでは、プライベートのメールアドレスを読み込むため、user:email
スコープをリクエストしています。
ブラウザでhttp://localhost: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
ヘッダも含まれています。 このヘッダには、リクエストを行うために使用されたトークンのスコープのリストが含まれています。 In addition to that, the OAuth Applications API provides an endpoint to check a token for validity. この情� �を使用してトークンのスコープにおける変更を検出し、利用可能なアプリケーション機能の変更をユーザに通知します。
認証リクエストの実施
最後に、このアクセストークンで、ログインしたユーザとして認証のリクエストを行うことができます。
# 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://localhost: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
コードの大部分は見慣れたもののはずです。 For example, we're still using RestClient.get
to call out to the GitHub Enterprise Server API, and we're still passing our results to be rendered in an ERB template (this time, it's called 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://localhost:4567
に移動すると、アプリケーションはauthenticate!
を呼び出し、/callback
にリダイレクトします。 そして/callback
で/
に戻され、認証が終わっているのでadvanced.erbがレンダリングされます。
GitHub Enterprise ServerのコールバックURLを/
にする� けで、このラウンドトリップ経路を単純化できました。 た� し、server.rbとadvanced.rbの両方が同じコールバックURLに依存しているため、動作は少し不安定になります。
また、このアプリケーションをGitHub Enterprise Serverデータにアクセスするよう認証したことがない� �合、以前と同じ確認ダイアログが表示され、警告されるでしょう。