このセクションでは、認証の基本に焦点を当てます。 具体的には、アプリケーションのウェブフローを実装した、(Sinatra を使う) Rubyサーバーを、いくつかの方法で作成します。
このプロジェクトの完全なソースコードは、platform-samples リポジトリからダウンロードできます。
アプリケーションの登録
First, you'll need to register your application. 登録された各 OAuth アプリケーションには、一意のクライアント ID とクライアントシークレットが割り当てられます。 クライアントシークレットは共有しないでください。 共有には、文字列をリポジトリにチェックインすることも含まれます。
どのような情報を入力しても構いませんが、認証コールバック URL は例外です。 これが、アプリケーションの設定にあたってもっとも重要な情報と言えるでしょう。 認証の成功後に GitHub Enterprise Server がユーザに返すのは、コールバックURLなのです。
通常の Sinatra サーバーを実行しているので、ローカルインスタンスの場所は http://localhost:4567
に設定されています。 コールバック URL を http://localhost:4567/callback
と入力しましょう。
ユーザ認証の承認
さて、簡単なサーバーの入力を始めましょう。 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
Your client ID and client secret keys come from your application's configuration page. We recommend storing them as environment variables--which is exactly what we've done here.
次に、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!</a>
</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にアクセスすることは、おそらくないということに留意してください。 もっと本格的なアプリケーションであれば、お好みの言語で書かれたライブラリを使った方がいいでしょう。
付与されたスコープの確認
Users can edit the scopes you requested by directly changing the URL. This can grant your application less access than you originally asked for. Before making any requests with the token, check the scopes that were granted for the token by the user. For more information about requested and granted scopes, see "Scopes for OAuth Apps."
付与されたスコープは、トークンの交換によるレスポンスの一部として返されます。
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][/v3/apps/oauth_applications/#check-an-authorization]. この情報を使用してトークンのスコープにおける変更を検出し、利用可能なアプリケーション機能の変更をユーザに通知します。
認証リクエストの実施
最後に、このアクセストークンで、ログインしたユーザとして認証のリクエストを行うことができます。
# 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
に直接移動してみてください。 エラーになるでしょう。
What if we could circumvent the entire "click here" process, and just remember that, as long as the user's logged into GitHub Enterprise Server, they should be able to access this application? Hold on to your hat, because that's exactly what we're going to do.
上記に上げたサーバはかなり単純なものです。 インテリジェントな認証を入れるために、トークンを保存するためセッションを使用するよう切り替えます。 これにより、認証はユーザーに意識されないものになります。
また、セッション内のスコープを永続的にしているため、そのスコープを確認した後にユーザが更新した場合や、トークンを取り消した場合に対処する必要があります。 これを行うために、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://localhost:4567
に移動すると、アプリケーションはauthenticate!
を呼び出し、/callback
にリダイレクトします。 そして/callback
で/
に戻され、認証が終わっているのでadvanced.erbがレンダリングされます。
GitHub Enterprise ServerのコールバックURLを/
にするだけで、このラウンドトリップ経路を単純化できました。 ただし、server.rbとadvanced.rbの両方が同じコールバックURLに依存しているため、動作は少し不安定になります。
また、このアプリケーションをGitHub Enterprise Serverデータにアクセスするよう認証したことがない場合、以前と同じ確認ダイアログが表示され、警告されるでしょう。