WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

Google アカウントの認証を OpenID から OpenID Connect に移行する方法

なんと 10 ヶ月ぶりの投稿になってしまいました・・・。これまでは空き時間のほとんどを Feedeen (フィードリーダー)の強化に回していて、 Evernote への投稿を実装したり埋め込み動画の再生に対応したりタブレット版を追加したりしてました。現在は招待コードなしで登録できるようになっているので、興味のある方はぜひ使ってみてください。

さて、その Feedeen ですが、先日ひそかにユーザー認証の方法を OpenID 2.0 から OpenID Connect に移行しました。 Google の OpenID 2.0 認証は来年の 4 月に廃止予定となっており、 OpenID Connect への移行が推奨されているためです。

OpenID Connect への移行はやってみればとても簡単でした。 OpenID Connect での認証時にパラメータをひとつ追加するだけで OpenID 2.0 のユーザー識別子を一緒に返してくれるので、ユーザーに手間を掛けることもなく(承認画面が一回表示されるだけ)、ほぼシームレスに移行できます。

せっかくですので、ドキュメントの説明不足なところを補いつつ、移行に必要な情報をまとめてみました。 OpenID Connect での認証処理(および OpenID 2.0 のユーザー識別子の取得)を行う Ruby のクラスも記事末に付けましたので、これから OpenID Connect に移行する方は、ぜひ参考にしてください。

Client ID を取得する

それでは、 OpenID Connect で Google アカウントを認証する方法を説明していきます。まず、 Google Developer Console を使って、 Client ID と Client Secret を取得します。このあたりは OAuth 2.0 と同じなので、 Google の API を使っている人にはお馴染みでしょう。

Web ブラウザで Google Developer Console を開き、適当にプロジェクトを作ってください。使えるものがあれば、既存のプロジェクトでもかまいません。そして、プロジェクトを選択 > APIs & auth > Credentials と進んで、「CREATE NEW CLIENT ID」で Client ID を作成します。

Application Type は Web application を選択し、 Authorized JavaScript origins にサイトの URL (パスは含まない)、 Authorized redirect URI に 認証後のコールバック先 URL をそれぞれ入力してください。 URL は行を分けて複数入力できます。「Create Client ID」をクリックすると、新しい Client ID が作成されます。

いろいろ表示されていますが、 OpenID Connect で使用するのは Client ID と Client secret のみです。

認証ページヘのリダイレクト

ここからは、実際の処理の流れをみていきましょう。認証の開始は、ユーザーを https://accounts.google.com/o/oauth2/auth にリダイレクトするだけです。その際、以下のクエリーパラメータを渡します(完全なパラメータのリストはこちら)。

パラメータ名説明
client_idDeveloper Console で取得した Client ID
scope要求する権限
response_type常に code を指定
redirect_uri認証後にユーザーがリダイレクトされる URL
stateCSRF を防ぐためのトークン(通常はランダムな文字列を指定し、後に照合する)
openid.realmOpenID 認証で指定していた realm

scope には複数の権限を空白区切りで指定するのですが、最低限 "openid" を入れて、必要に応じて "email" (メールアドレスの取得に必要) や "profile" (people.getOpenIdConnect でユーザー名などを取得するのに必要)を加えます。 Google+ Sign-in のスコープ (https://www.googleapis.com/auth/*) を指定してしまうと Google+ を有効にしていないユーザーが認証できなくなるので、注意してください。

また、 OpenID 2.0 からの移行では、 openid.reald に OpenID 2.0 で使っていた realm とまったく同じ文字列を指定してください。こうすることで OpenID Connect と OpenID 2.0 の両方のユーザー識別子を取得でき、それらを結びつけることができます。

例えば Feedeen へのログインでは、以下の URL へユーザーをリダイレクトしています。

https://accounts.google.com/o/oauth2/auth?
  client_id=<取得したClient ID>&
  scope=openid%20email%20profile&
  response_type=code&
  redirect_uri=http%3A%2F%2Ffeedeen.com%2Fsession&
  state=<ランダムな文字列>&
  openid_realm=http%3A%2F%2Ffeedeen.com%2F

コールバックを受ける

上記の URL にユーザーをリダイレクトすると、ユーザーに承認を要求するページが表示されます。

ユーザーは承認したかキャンセルしたかに関わらず、 redirect_uri にリダイレクトされます。その際、以下のクエリーパラメータが付加されます。

パラメータ名説明
codeアクセストークンを取得するための一時的なトークン
stateリダイレクトの際のstateパラメータと同じ文字列
errorエラーメッセージ

コールバックでの処理は、概ね以下の手順になります。

  1. state パラメータの値が、認証開始時に指定したものと等しいことを確かめる。
  2. error パラメータが設定されていたら、処理を中断する(主にユーザーがキャンセルした場合)。
  3. アクセストークン等を取得。
  4. id_token をパースして OpenID のユーザー識別子等を取得。
  5. ユーザーのプロフィール情報を取得。

(1), (2) は単にパラメータを比較するだけなので問題ないでしょう。 (3) から順番に説明していきます。

アクセストークンを取得する

コールバックに渡される code パラメータはアクセストークンを取得するための引換券のようなものなので、そのままでは API アクセスに使えません。アクセストークンを取得するには、以下の値を application/x-www-form-urlencoded の形式で https://accounts.google.com/o/oauth2/token に POST します。

パラメータ名説明
codeコールバックに渡されたcodeパラメータ
client_idDeveloper Console で取得した Client ID
client_secretDeveloper Console で取得した Client secret
redirect_uri認証ページに渡した redirect_uri と同じ文字列
grant_type常に authorization_code を指定

例えば Feedeen へのログインでは以下のようなリクエストを送信しています。

POST /o/oauth2/token HTTP/1.1
Host: accounts.google.com
Content-Type: application/x-www-form-urlencoded

code=<渡された code の値>&
client_id=<取得した Client ID>&
client_secret=<取得した CLient secret>&
redirect_uri=http%3A%2F%2Ffeedeen.com%2Fsession&
grant_type=authorization_code

このリクエストが成功すると、レスポンスとして以下のフィールドを持つ JSON 形式のデータが返ります。

パラメータ名説明
access_tokenAPI アクセスのためのアクセストークン
id_tokenユーザーの情報を格納した JWT (JSON Web Token)
expires_inアクセストークンの有効期限
token_type常に Bearer が設定される

基本的には OAuth 2.0 のコールバックと同じですが、 id_token が独特ですね。この id_token の中に、ユーザーの識別子をはじめとした OpenID Connect 独自の情報が格納されています。

id_token をパースする

アクセストークンと一緒に取得した id_token は JSON Web Token と呼ばれる形式のデータで、 こちらのページ でわかりやすく解説されてます。要は JSON データとその署名を URL-safe BASE 64 でエンコードして、ピリオド区切りで並べたものです。なので、単にデータを取り出したいだけなら、ピリオドで区切られた 2 番目の文字列を URL-safe BASE 64 としてデコードすれば取得できます。ただ、 Google が公開している gem なら署名の検証などもしてくれるので、そちらを使うのが良いでしょう。

使い方は、 GoogleIDToken::Validator#check に、 id_token と Client ID を渡すだけです。格納されていた JSON データが返ります。

validator = GoogleIDToken::Validator.new
data = validator.check(id_token, client_id)

なお、 google-api-client の gem が入っているなら、そちらの Google::APIClient#verify_id_token! を使う手もあります。

パース後のデータにはいろいろフィールドがありますが、主に必要なのは以下の 3 つです。

パラメータ名説明
emailユーザーのメールアドレス(scope に email を指定したときのみ)
email_verifiedemail が検証済みなら true
subユーザーのユニークな識別子
openid_idOpenID 2.0 のユーザー識別子(openid.realm を指定したときのみ)

ここでようやく OpenID 2.0 のユーザー識別子が手に入りました。 OpenID 2.0 から OpenID Connect への移行の際は、 sub と openid_id の両方でユーザーを検索し、ヒットしたユーザーに sub を設定してやる、ということになるでしょう。あとは、一人でも多くのユーザーに最低一回はログインしてもらうように促しましょう。

ユーザーのプロファイル情報を取得

ここは scope に profile を指定した場合のみとなりますが、 people.getOpenIdConnect を呼び出すことで、ユーザー名等のプロフィール情報を取得できます。この API は Google+ API の一部という扱いなので、 Google Developer Console で Google+ API を有効にしておいてください。

具体的には、 https://www.googleapis.com/plus/v1/people/me/openIdConnect に GET リクエストを送ります。その際、 Authorization ヘッダにアクセストークンを設定する必要があります。したがって、実際のリクエストは以下のようになります。

GET /plus/v1/people/me/openIdConnect HTTP/1.1
Host: www.googleapis.com
Authorization: OAuth <アクセストークン>

レスポンスは JSON 形式で、以下のフィールドがあります。

パラメータ名説明
kindplus#personOpenIdConnect に固定
gendermale, female, other のいずれか
subユーザーのユニークな識別子
nameユーザーのフルネーム
family_nameユーザーの姓
given_nameユーザーの名
profileユーザーのプロフィールページの URL
pictureユーザーのアバター画像の URL
emailユーザーのメールアドレス
email_verifiedemail が検証済みなら true
hdGoogle Apps ユーザーなら、そのドメイン名
localeユーザーの優先ロケール

ただし、ユーザーによっては取得できない項目もあるので注意してください。とくに email フィールドは、この API より id_token のほうが信頼できます。

ライブラリとサンプル

スクラッチで OpenID Connect を利用したアプリケーションを構築するなら、 openid_connect のような gem を利用するわけですが、 Feedeen では既存の実装をなるべく変えたくなかったので、独自にクラスを実装しました。なるべく多くのシチュエーションで使えるように、かなりベタな実装(クラスというよりは単なる関数の集まり)になっています。同様に gem の導入が厳しい場合には利用していただければ幸いです(とはいえ、 Google の google-id-token gem は必要ですが)。

https://raw.githubusercontent.com/webos-goodies/op...

require 'erb'
require 'securerandom'
require 'net/https'
require 'uri'
require 'json'

class OpenIDConnect
  class BaseError < StandardError; end
  class InvalidTokenError < BaseError; end
  class CancelError < BaseError; end
  class ExchangeError < BaseError; end
  class IdTokenError < BaseError; end

  def initialize(session, redirect_uri)
    @session      = session
    @redirect_uri = redirect_uri
  end

  def authentication_url(scope='', realm=nil)
    scope = [*(scope || '')].reject(&:blank?).map(&:to_s).join(' ')
    @session[:openid_state] = SecureRandom.urlsafe_base64
    p = ["state=#{@session[:openid_state]}",
         "redirect_uri=#{ERB::Util.u(@redirect_uri)}",
         "response_type=code",
         "client_id=#{ERB::Util.u(config['key'])}"]
    p << "scope=#{ERB::Util.u(scope)}" unless scope.empty?
    p << "openid.realm=#{ERB::Util.u(realm)}" unless realm.blank?
    "https://accounts.google.com/o/oauth2/auth?#{p.join('&')}"
  end

  def authentication_result(params)
    if !@session[:openid_state] || @session[:openid_state] != params[:state]
      raise InvalidTokenError
    elsif params[:error]
      raise CancelError
    end

    p = {
      code:          params[:code],
      client_id:     config['key'],
      client_secret: config['secret'],
      redirect_uri:  @redirect_uri,
      grant_type:    'authorization_code'
    }

    uri  = URI.parse('https://accounts.google.com/o/oauth2/token')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.start do
      req = Net::HTTP::Post.new(uri.path)
      req.set_form_data(p)
      res = http.request(req)
      raise ExchangeError unless res.kind_of?(Net::HTTPSuccess)
      JSON.load(res.body)
    end
  end

  def parse_id_token(result)
    validator = GoogleIDToken::Validator.new
    payload = validator.check(result['id_token'], config['key'])
    raise IdTokenError unless payload
    raise IdTokenError unless payload['sub']
    payload
  end

  def retrieve_profile(result)
    uri  = URI.parse('https://www.googleapis.com/plus/v1/people/me/openIdConnect')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.start do
      hdr = { 'Authorization' => "OAuth #{result['access_token']}" }
      res = http.get(uri.path, hdr)
      if res.kind_of?(Net::HTTPSuccess)
        JSON.load(res.body)
      else
        {}
      end
    end
  rescue
    {}
  end

  private

  @@config = nil
  def config
    unless @@config
      @@config = YAML.load_file(File.join(Rails.root, 'config/openid_config.yml'))
    end
    @@config
  end

end

以下はこのクラスを使って OpenID Connect 認証を行うコントローラの例です。クラスの使い方は、これを見れば概ねわかっていただけるのではないかと思います。

# -*- coding: utf-8 -*-

require 'openid-connect'

class SessionsController < ApplicationController

  # ログインの開始(権限の承認ページヘのリダイレクト)
  def new
    reset_session

    # OpenIDConnectクラスのインスタンスを作成。
    # 引数はRailsのセッションオブジェクトとコールバックURL
    oc = OpenIDConnect.new(session, session_url)

    # 承認ページのURLを構築し、そこへリダイレクトする。
    # 引数は、スコープと OpenID 2.0 の realm。
    redirect_to oc.authentication_url('openid email profile', root_url)
  end

  # コールバックの処理
  def show
    # OpenIDConnectクラスのインスタンスを作成。
    # 引数はRailsのセッションオブジェクトとコールバックURL
    oc      = OpenIDConnect.new(session, session_url)

    # 引き渡された code をアクセストークンに交換する
    # 引数はクエリーパラメータを格納したHash
    @result = oc.authentication_result(params)

    # id_tokeをパースして中身を返す
    # 引数はauthentication_resultの返り値
    @payload = oc.parse_id_token(@result)

    # プロファイル情報を取得
    # 引数はauthentication_resultの返り値
    @profile = oc.retrieve_profile(@result)

    @error = nil

  # エラーが発生したときは以下の例外が投げられる
  rescue OpenIDConnect::CancelError
    @error = "認証がキャンセルされました"
  rescue OpenIDConnect::InvalidTokenError
    @error = "state パラメータが異なっています"
  rescue OpenIDConnect::ExchangeError
    @error = "アクセストークンの取得に失敗しました"
  rescue OpenIDConnect::IdTokenError => e
    @error = "id_tokenのパースに失敗しました"
  end

end

また、上記のクラスを利用して OpenID Connect 認証を行うだけの Rails アプリも作ってみました。

https://github.com/webos-goodies/openid-connect-sa...

これを実行して、認証を行うと、コールバックへのパラメータ、アクセストークン取得時のレスポンス、 id_token のパース結果、プロファイル取得時のレスポンスを以下のような感じで表示します。プロトコルの把握にも役立つのではないかと思います。

ということで、 OpenID Connect 簡単でいい感じです。まだ移行していない方は、ぜひこの機会に移行しましょう。まだ廃止まで 1 年近くあるとはいえ、ギリギリになってからの移行はリスクがありますからね。この記事が少しでも助けになれば幸いです。

それでは、また。

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles