Google OpenID Connect の 実装方法

0 件のコメント

今回は「Googleを例にOpenID Connectの実処理フロー」についてまとめます。 今回も、OpenID Connect すべてをカバーするのではなく、一番サポート率が高く使われるであろうユースケース「BasicOP(response_type: code)」についてのみを対象にまとめます。

概要

今回は Google の OpenID Connect / OAuth 2.0 を使ってみることで、具体的な OpenID Connect の実装イメージを持てるようにしていきます。

OpenID Connect の大まかな処理フローは以下のようなイメージです。

  1. 利用アカウント(OpenID)選択
  2. ログイン & 許可
  3. リダイレクト with 認可コード
  4. トークンリクエスト with 認可コード → アクセストークン、IDトークン
  5. 個人情報リクエスト with アクセストークン → 個人情報

概要レベルの話は別記事(OpenID Connect の 概要)を書いていますのでそちらも参照してみてください。

ただ…上記の図に示されているのは「OpenIDを利用する場合の処理フロー」のみです。 実際は「事前にOpenID提供事業者に対して登録処理」が必要になります。 ここで事前の登録処理も含めた全体を対象にまとめていきます。

  • 事前準備(OpenID提供側サーバー設定)
  • クライアント実装(OpenID利用側処理フロー)

事前準備

OpenID や OAuth を利用する場合、あらかじめ OpenID 提供サービス側に対して登録作業が必要になります。

Googleの OpenID Connect ないし OAuth を利用したい場合、 Google Cloud Console (GCP) の 「APIとサービス」 においてあらかじめ設定をしておきます。 GCPを利用するにはあらかじめプロジェクトの作成や支払い設定が必要になりますが…ここでは割愛します(=プロジェクト作成と支払い設定は終わっている前提)。

事前設定を行うのは以下の2か所になります。

  • OAuth同意画面
  • 認証情報

「Googleでログイン」を選んだあと表示される「○○がGoogleアカウントへのアクセスをリクエストしています」画面の設定です。

  1. 「APIとサービス」の左メニューから「OAuth同意画面」を選択

  2. 「外部」を選択して「作成」

  3. 以下の設定をして「保存」

    アプリケーション名
    必須。エンドユーザーがGoogleのログイン画面へ遷移してきたとき表示される。
    アプリケーションのロゴ
    任意。
    サポートメール
    いずれかを選択。
    Google API のスコープ
    ログインさせるだけならデフォルトのまま(email, profile, openid)でOK。
    承認済みドメイン
    テストだけなら空欄でOK。本番環境あるなら本番のドメインを指定。
    その他リンク
    任意。

エンドユーザーがGoogleのアカウント情報を提供することを許可したあと、「どういった方法でOpenIDを受け渡しするのか」と「どこへリダイレクトさせるか」の設定です。

  1. 「APIとサービス」の左メニューから [認証情報] を選択

  2. 上部メニュー [認証情報を作成]-[OAuthクライアントID] を選択

  3. 以下の設定をして「保存」

    アプリケーションの種類
    今回は response_type: core にするので「ウェブアプリケーション」。
    名前
    適当な名前に変更します。
    承認済みのJavaScript生成元
    基本は空欄。JavaScript実装しない(= XSSを気にしなくてよいシステムにする)ので。
    承認済みのリダイレクトURI
    テストようであれば http://localhost:8080/ などを入力。 ここに設定する値はこのあと出てくる redirect_uri とプロトコル(http or https)や / 含めて一致している必要があるので要注意。
  4. 完了画面に表示される「クライアントID」「クライアントシークレット」を保存

完了画面で「クライアントID」と「クライアントシークレット」をメモし忘れていても、以下の手順で確認できます。

  1. 「OAuthクライアントID」から確認した「クライアントID」の名前を選択

  2. 画面右側に「クライアントID」と「クライアントシークレット」が表示されるのでこれらをメモ

クライアント実装

前述の事前準備が終わっていれば、あとはクライアント側(実際にOpenIDを利用してサービス提供したい側)の実装になります。 Google自体は専用のライブラリを出していてそちら を利用することが推奨されていますが…今回は処理フローをおさえたいのであえて独自でやってみます。

概要

シーケンス図を使っておおまかな流れを確認しておきます。 よく見かける処理の概略図(最初に掲載したのも含めて)だけだとわかりにくい部分があるので、きちんとリクエストレスポンスの組み合わせで記述しきってみました。

詳細

クライアントが提供する「ログイン画面」を表示します。 この「ログイン画面」にはいわゆる「Googleでログイン」的なボタンが含まれています。 以下はイメージ図。

「Googleでログイン」ボタンを押下するとGoogleのログイン画面に遷移します。 このときリクエストするAPIおよびパラーメーター(「Googleでログイン」ボタンを押下した時に遷移する先)は以下の通りです。

HTTPリクエスト

GET https://accounts.google.com/o/oauth2/v2/auth
パラメーター 必須 説明
client_id 必須 事前準備の「認証情報設定」で取得した「クライアントID」を指定。
response_type 必須 OpenID Connect で指定されているレスポンスタイプを指定。今回のフローは code 。 選択できる値は以下。
  • code
  • token
  • id_token
  • id_token token
  • code id_token
  • code token
  • code id_token token
  • none
scope 必須 事前準備の「OAuth同意画面」で指定した「Google API の スコープ」を指定。 複数指定する場合は半角スペースでつないで指定。 OpenID Connect の場合、 openid を必ず含める。 今回は openid, email, profile
redirect_uri 必須 事前準備の「認証情報設定」で指定した「承認済みのリダイレクトURI」のうちのいずれかを指定。指定するURIは完全一致なので要注意。
nonce 必須 ランダムな値。 リプレイ攻撃対策。
state 推奨 ランダムな値。 CSRF対策。
access_type   以下のいずれかを選択。
  • offline
  • online
prompt   以下から選択。複数選択する場合は空白でつなぐ。
  • none

    認可サーバー側で認証画面や承諾画面を表示。 エンドユーザーが事前承諾していなかったり、リクエスト情報に不足があった利する場合、エラーで返す。

  • consent

    認可サーバー側でエンドユーザーに対して承諾画面を表示。

  • select_account

    認可サーバー側でアカウント選択画面を表示。

サンプル

https://accounts.google.com/o/oauth2/v2/auth?
  client_id=348089316378.apps.googleusercontent.com&
  response_type=code&
  scope=openid%20email%20profile&
  redirect_uri=http://localhost:8080/&
  nonce=eW4VCp2R&
  state=Xw4Vf6Yh

参考

ログイン画面が表示されたらエンドユーザーはID、パスワード、2段階認証を入力します。

エンドユーザーが「情報開示の許可」を行うと「認証情報設定」の「承認済みのリダイレクトURI」で指定したURI(クライアント)にリダイレクトします。

このリダイレクト後、クライアントが受け取るリクエストは以下のようなものになります。 今回はリダイレクト先が http://localhost:8080 なのでリクエスト先も localhost です。 おさえておきたいのは「 code パラーメーターに認可コードが入っている」という点です。

HTTPリクエスト

GET http://localhost:8080/
パラメーター 必須 説明
code   認可コード。 トークンエンドポイントに対するリクエストで利用する。 response_typecode が含まれていれば取得できる。

サンプル

http://localhost:8080/?
  state=Xw4Vf6Yh&
  code=4%2FvAHuTzYJdTF0USpYZnE0QrrrQcu9uNehLga_WjUF02GDm3vE326PJpAokqLB4WCCvPUzfjry0qPmAUaNK3bCoyw&
  scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&
  authuser=0&
  session_state=bf96cbcd87c4297bbd8d1db4c5f7294521b04868..8cca&
  prompt=consent

「認可コード」を受け取ることができれば「トークンエンドポイント」に対してリクエストを投げることで「アクセストークン」および「IDトークン」を取得できます。 このときのリクエストおよびレスポンスは以下のようなものになります。

HTTPリクエスト

POST https://oauth2.googleapis.com/token
パラメーター 必須 説明
code 必須 認可コード。 認可リクエストのレスポンス(リダイレクト)に含まれる認可コード。
client_id 必須 クライアントID。 事前準備の認証情報設定で取得したクライアントID。
client_secret 必須 クライアントパスワード。 事前準備の認証情報設定で取得したクライアントシークレット。
redirect_uri 必須 リダイレクトURI。 事前準備の認証情報設定で設定した承認済みのリダイレクトURI。
grant_type   authorization_code 固定。

サンプル

POST https://oauth2.googleapis.com/token HTTP/1.1
User-Agent: Fiddler
Content-Type: application/x-www-form-urlencoded
Host: oauth2.googleapis.com
Content-Length: 292

code=4/vAHuTzYJdTF0USpYZnE0QrrrQcu9uNehLga_WjUF02GDm3vE326PJpAokqLB4WCCvPUzfjry0qPmAUaNK3bCoyw&
client_id=348089316378.apps.googleusercontent.com&
client_secret=************&
redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F&
grant_type=authorization_code

レスポンス

パラメーター 説明
access_token アクセストークン
expires_in アクセストークンの有効期間(秒)
id_token IDトークン
token_type 返却したトークンの種類。Bearer 固定。
refresh_token リフレッシュトークン。 access_type: offline のときに返却される。

サンプル

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Vary: X-Origin
Vary: Referer
Date: Sun, 05 Jan 2020 02:44:28 GMT
Server: scaffolding on HTTPServer2
Cache-Control: private
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Alt-Svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
Accept-Ranges: none
Vary: Origin,Accept-Encoding
Content-Length: 1563

{
  "access_token": "ya29.Il-4B_UUTvXwxGRLfV3W_4Uw-wJB3SjBGkuEj1foJFKSNE0B9wsTFZCNv7w6Q8uLYgxKEGECEeeWF0RiZMsbxZxijj21uGEcB4gEjys8J2ewydqtLCdgSMKbeGujDqHL8w",
  "expires_in": 3600,
  "scope": "https://www.googleapis.com/auth/userinfo.email openid https://www.googleapis.com/auth/userinfo.profile",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjdoemN2eHFuZ25idTMzZ3B4MzlzYWFxMmF1cnEyZnY4bXg5cTM1bTciLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIzNDgwODkzMTYzNzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIzNDgwODkzMTYzNzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiI5MzY3Njc2OTgzODIzNTgzMjM0NjkiLCJlbWFpbCI6InRzdXlvc2hpLnRhbmFrYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IjJqb1ZFOHpDM3BCSnlDaDNvSzlhRkEiLCJub25jZSI6ImVXNFZDcDJSIiwibmFtZSI6IueUsOS4reWJmyIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vLU1RWmtDcjZrQVVrL0FBQUFBQUFBQUFJL0FBQUFBQUFBQUFBL1BrVDhCM2puYWdBWE5yWlE5cWFXR2RVZE1FbmVMOVB3NXgvczk2LWMvcGhvdG8uanBnIiwiZ2l2ZW5fbmFtZSI6IuWJmyIsImZhbWlseV9uYW1lIjoi55Sw5LitIiwibG9jYWxlIjoiamEiLCJpYXQiOjE1NzgxOTIyNjgsImV4cCI6MTU3ODE5NTg2OH0.VRxXxVi9Vw05zURdrR9RoCO3bCpPxUDteoVy8SaFhGU0Y0TjMH6eG0Jfqrz4Epdm82-22_xiIGnA7GEMdnxnbPiuZAfpYWTdmC_LmgY8Ww5GfFH6YkLNL0pmZGuyXtQ_Fzn5T9hUVhQTELw-6IuiTNyfCEaLbqFy7lqmlaox5xDVYSyZ42dUL3Z5sY5DEfSi_o52SSBOkmLtcHMb74rtzxhkn36WEYqcnGy9R3q1S5H8hoQw4jsd7qV79RwUtiCRFoMRyt_yu4YiJYs02ANTE0-Q9zcY7LsIovM0QL_SLIs28nHbfFR-aeK1qV9bpcHM7nVlMQv8fD1wK_66TavSOQ"
}

id_token (IDトークン) はJWT形式のデータなので、Base64URLデコードを行うと中身を取り出すことができます。 署名は展開できないので、それ以外のヘッダーとペイロードについて展開したものを以下に載せます。

  "id_token": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjdoemN2eHFuZ25idTMzZ3B4MzlzYWFxMmF1cnEyZnY4bXg5cTM1bTciLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIzNDgwODkzMTYzNzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIzNDgwODkzMTYzNzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiI5MzY3Njc2OTgzODIzNTgzMjM0NjkiLCJlbWFpbCI6InRzdXlvc2hpLnRhbmFrYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IjJqb1ZFOHpDM3BCSnlDaDNvSzlhRkEiLCJub25jZSI6ImVXNFZDcDJSIiwibmFtZSI6IueUsOS4reWJmyIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vLU1RWmtDcjZrQVVrL0FBQUFBQUFBQUFJL0FBQUFBQUFBQUFBL1BrVDhCM2puYWdBWE5yWlE5cWFXR2RVZE1FbmVMOVB3NXgvczk2LWMvcGhvdG8uanBnIiwiZ2l2ZW5fbmFtZSI6IuWJmyIsImZhbWlseV9uYW1lIjoi55Sw5LitIiwibG9jYWxlIjoiamEiLCJpYXQiOjE1NzgxOTIyNjgsImV4cCI6MTU3ODE5NTg2OH0.VRxXxVi9Vw05zURdrR9RoCO3bCpPxUDteoVy8SaFhGU0Y0TjMH6eG0Jfqrz4Epdm82-22_xiIGnA7GEMdnxnbPiuZAfpYWTdmC_LmgY8Ww5GfFH6YkLNL0pmZGuyXtQ_Fzn5T9hUVhQTELw-6IuiTNyfCEaLbqFy7lqmlaox5xDVYSyZ42dUL3Z5sY5DEfSi_o52SSBOkmLtcHMb74rtzxhkn36WEYqcnGy9R3q1S5H8hoQw4jsd7qV79RwUtiCRFoMRyt_yu4YiJYs02ANTE0-Q9zcY7LsIovM0QL_SLIs28nHbfFR-aeK1qV9bpcHM7nVlMQv8fD1wK_66TavSOQ"

↓ Base64URLデコード

// ヘッダー
{
  "alg": "HS256",
  "kid": "7hzcvxqngnbu33gpx39saaq2aurq2fv8mx9q35m7",
  "typ": "JWT"
}

// ペイロード
{
  "iss": "https://accounts.google.com",
  "azp": "348089316378.apps.googleusercontent.com",
  "aud": "348089316378.apps.googleusercontent.com",
  "sub": "936767698382358323469",
  "email": "tsuyoshi.tanaka@gmail.com",
  "email_verified": true,
  "at_hash": "2joVE8zC3pBJyCh3oK9aFA",
  "nonce": "eW4VCp2R",
  "name": "田中剛",
  "picture": "https://lh3.googleusercontent.com/-MQZkCr6kAUk/AAAAAAAAAAI/AAAAAAAAAAA/PkT8B3jnagAXNrZQ9qaWGdUdMEneL9Pw5x/s96-c/photo.jpg",
  "given_name": "剛",
  "family_name": "田中",
  "locale": "ja",
  "iat": 1578192268,
  "exp": 1578195868
}

参考

追加の個人情報が欲しい場合は「個人情報エンドポイント」に対して「アクセストークン」を付けてリクエストします。

HTTPリクエスト

GET https://www.googleapis.com/userinfo/v2/me
ヘッダー 説明
Authorization Bearerスキーマにトークンリクエストで取得したアクセストークンを指定。

サンプル

GET https://www.googleapis.com/userinfo/v2/me HTTP/1.1
Host: www.googleapis.com
Content-length: 0
Authorization: Bearer ya29.Il-4B_UUTvXwxGRLfV3W_4Uw-wJB3SjBGkuEj1foJFKSNE0B9wsTFZCNv7w6Q8uLYgxKEGECEeeWF0RiZMsbxZxijj21uGEcB4gEjys8J2ewydqtLCdgSMKbeGujDqHL8w

レスポンス

パラメーター 説明
name エンドユーザーの名前。
given_name エンドユーザーの名(下の名前)。
family_name エンドユーザーの姓(上の名前)。
nickname エンドユーザーのニックネーム。
profile エンドユーザーのプロフィールページURL。
picture エンドユーザーのプロフィール画像URL。
email エンドユーザーのメールアドレス。
gender エンドユーザーの姓別。
birthdate エンドユーザーの誕生日。YYYY-MM-DDフォーマット。
locale エンドユーザーのロケール。
address エンドユーザーの住所。
updated_at エンドユーザーの情報が更新された日付(エポック秒)。

サンプル

HTTP/1.1 200 OK
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Pragma: no-cache
Date: Sun, 05 Jan 2020 02:49:41 GMT
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json; charset=UTF-8
Vary: X-Origin
Vary: Referer
Server: ESF
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Alt-Svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
Accept-Ranges: none
Vary: Origin,Accept-Encoding
Content-Length: 322

{
  "id": "936767698382358323469",
  "email": "tsuyoshi.tanaka@gmail.com",
  "verified_email": true,
  "name": "田中剛",
  "given_name": "剛",
  "family_name": "田中",
  "picture": "https://lh3.googleusercontent.com/-MQZkCr6kAUk/AAAAAAAAAAI/AAAAAAAAAAA/PkT8B3jnagAXNrZQ9qaWGdUdMEneL9Pw5x/s96-c/photo.jpg",
  "locale": "ja"
}

参考

今回は Google の OpenID Connect を例に 「OpenID Connect の具体的な実装方法」 についてまとめました。 参考になったでしょうか? 本記事がお役に立っていると嬉しいです!!

参考記事

最後に… このブログに興味を持っていただけた方は、 ぜひ 「Facebookページ に いいね!」または 「Twitter の フォロー」 お願いします!!