Skip to main content

認証の基本

さまざまな認証方法について、いくつかの例で学びます。

このセクションでは、認証の基本に焦点を当てます。 具体的には、アプリケーションのウェブフローを実装した、(Sinatra を使う) Rubyサーバーを、いくつかの方法で作成します。

このプロジェクトの完全なソースコードは、platform-samples リポジトリからダウンロードできます。

アプリケーションの登録

まず、アプリケーションの登録が必要です。 登録された各 OAuth アプリケーションには、一意のクライアント ID とクライアントシークレットが割り当てられます。 クライアントシークレットは共有しないでください。 共有には、文字列をリポジトリにチェックインすることも含まれます。

どのような情報を入力しても構いませんが、認証コールバック URL は例外です。 これが、アプリケーションの設定にあたってもっとも重要な情報と言えるでしょう。 認証の成功後に GitHub がユーザに返すのは、コールバックURLなのです。

通常の Sinatra サーバーを実行しているので、ローカルインスタンスの場所は http://localhost:4567 に設定されています。 コールバック URL を http://localhost:4567/callback と入力しましょう。

ユーザ認証の承認

非推奨の注意: GitHubは、クエリパラメータを使ったAPIの認証を廃止します。 APIの認証はHTTPの基本認証で行わなければなりません。APIの認証にクエリパラメータを使用することは、2021年5月5日以降できなくなります。 予定された一時停止を含む詳しい情報についてはブログポストを参照してください。

さて、簡単なサーバーの入力を始めましょう。 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 とクライアントシークレットは、アプリケーションの設定ページから取得されます。 You should never, ever store these values in GitHub や、それに限らず公開の場に保存しないでください。これらは 環境変数として保存することをお勧めします。この例でも、そのようにしています。

次に、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に移動し、以下のようなダイアログが表示されます。 GitHubのOAuthプロンプト

あなた自身を信用する場合は、[Authorize App]をクリックします。 おっと、 Sinatraが404エラーを吐き出しました。 いったい何が起こったのでしょうか。

さて、コールバックURLをcallbackに指定したときのことを覚えていますか。 そのときルートを設定しなかったので、GitHubはアプリケーションを認証した後、ユーザをどこにドロップするかがわからなかったのです。 では、この問題を解決しましょう。

コールバックの設定

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は一時的なcode値を提供します。 このコードを、access_tokenと引き換えに、POSTでGitHubに戻す必要があります。 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('https://api.github.com/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('https://api.github.com/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 にログインしている限りそれを記憶して、このアプリケーションにアクセスできるとしたらどうでしょうか。 実のところ、 これからやろうとしていること_はまさにそういうことなのです。

上記に上げたサーバはかなり単純なものです。 インテリジェントな認証を入れるために、トークンを保存するためセッションを使用するよう切り替えます。 これにより、認証はユーザーに意識されないものになります。

また、セッション内のスコープを永続的にしているため、そのスコープを確認した後にユーザが更新した場合や、トークンを取り消した場合に対処する必要があります。 これを行うために、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('https://api.github.com/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('https://api.github.com/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 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のコールバックURLを/にするだけで、このラウンドトリップ経路を単純化できました。 ただし、server.rbadvanced.rbの両方が同じコールバックURLに依存しているため、動作は少し不安定になります。

また、このアプリケーションをGitHubデータにアクセスするよう認証したことがない場合、以前と同じ確認ダイアログが表示され、警告されるでしょう。