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_id | Developer Console で取得した Client ID |
scope | 要求する権限 |
response_type | 常に code を指定 |
redirect_uri | 認証後にユーザーがリダイレクトされる URL |
state | CSRF を防ぐためのトークン(通常はランダムな文字列を指定し、後に照合する) |
openid.realm | OpenID 認証で指定していた 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 | エラーメッセージ |
コールバックでの処理は、概ね以下の手順になります。
- state パラメータの値が、認証開始時に指定したものと等しいことを確かめる。
- error パラメータが設定されていたら、処理を中断する(主にユーザーがキャンセルした場合)。
- アクセストークン等を取得。
- id_token をパースして OpenID のユーザー識別子等を取得。
- ユーザーのプロフィール情報を取得。
(1), (2) は単にパラメータを比較するだけなので問題ないでしょう。 (3) から順番に説明していきます。
アクセストークンを取得する
コールバックに渡される code パラメータはアクセストークンを取得するための引換券のようなものなので、そのままでは API アクセスに使えません。アクセストークンを取得するには、以下の値を application/x-www-form-urlencoded の形式で https://accounts.google.com/o/oauth2/token に POST します。
パラメータ名 | 説明 |
---|---|
code | コールバックに渡されたcodeパラメータ |
client_id | Developer Console で取得した Client ID |
client_secret | Developer 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_token | API アクセスのためのアクセストークン |
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 つです。
パラメータ名 | 説明 |
---|---|
ユーザーのメールアドレス(scope に email を指定したときのみ) | |
email_verified | email が検証済みなら true |
sub | ユーザーのユニークな識別子 |
openid_id | OpenID 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 形式で、以下のフィールドがあります。
パラメータ名 | 説明 |
---|---|
kind | plus#personOpenIdConnect に固定 |
gender | male, female, other のいずれか |
sub | ユーザーのユニークな識別子 |
name | ユーザーのフルネーム |
family_name | ユーザーの姓 |
given_name | ユーザーの名 |
profile | ユーザーのプロフィールページの URL |
picture | ユーザーのアバター画像の URL |
ユーザーのメールアドレス | |
email_verified | email が検証済みなら true |
hd | Google 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 年近くあるとはいえ、ギリギリになってからの移行はリスクがありますからね。この記事が少しでも助けになれば幸いです。
それでは、また。
詳しくはこちらの記事をどうぞ!
この記事にコメントする