晴耕雨読

working in the fields on fine days and reading books on rainy days

Python/FlaskでOpenID Connectと通信する

PythonとFlaskを使って、GoogleのOpenID Connectと通信してユーザ情報を取得する方法について説明します。

手順

User   RelyingParty    IDProvider   UserInfo
 |          |               |          |
 | login    |               |          |
 |--------->|               |          |
 | redirect |               |          |
 |<---------|               |          |
 |          |               |          |
 |          :               |          |
 |       auth_request       |          |
 |------------------------->|*1        |
 |      login_to_Google     |          |
 |<------------------------>|          |
 |         redirect         |          |
 |<-------------------------|          |
 |          :               |          |
 |--------->|               |          |
 |          | token_request |          |
 |          |-------------->|*2        |
 |          |     token     |          |
 |          |<--------------|          |
 |          |                          |
 |          |--+                       |
 |          |  |verify                 |
 |          |<-+                       |
 |          |      request_profile     |
 |          |------------------------->|*3
 |          |        user_profile      |
 |          |<-------------------------|
 |          |                          |
  • User はアプリケーション利用者で、Googleアカウント所有者です。
  • Relying Party(リライングパーティ)はアプリケーションです。
  • ID Provider(IDプロバイダ)
    • *1 は認可エンドポイント (Authorization Endpoint)。Userはブラウザ画面でGoogleにログインします。
    • *2 はトークンエンドポイント (Token Endpoint)。アプリケーションが認可エンドポイントから受信したcodeを利用してトークンを取得します。
  • User Info
    • *3 はユーザ情報を返すエンドポイント。アプリケーションがとークンエンドポイントから受信したid_tokenを利用してユーザ情報を取得します。

事前準備

  • Google Cloud Consoleにて新規プロジェクトを作成し、公開ステータスを「本番」、認証情報のOAuth 2.0クライアントIDを作成し、承認済みのリダイレクトURIを設定します(今回は http://127.0.0.1:8000/callback)。
  • ********** には与えられたIDやシークレットや通信内容に応じて設定します。

PythonとFlaskによる実装

Python/Flaskによる実装はGitHubにて公開しています。

※車輪の再発明は学習目的にのみ行います。実運用では公式のライブラリなどを利用します。 Pythonの場合は次のライブラリを利用しましょう:googleapis/google-api-python-client: 🐍 The official Python client library for Google's discovery based APIs.

(1) ブラウザで以下のページにアクセスする(認可エンドポイントへのアクセス)

Googleログイン画面へのURLを用意して、アクセスさせる:

https://accounts.google.com/o/oauth2/v2/auth?client_id=929**********bro.apps.googleusercontent.com&response_type=code&scope=openid%20profile&&redirect_uri=https://127.0.0.1/callback&state=0123&nonce=4567
トップページ

「Login with Google」を押下する。

Googleログイン画面

(2) Googleアカウントでログインすると以下URLにリダイレクトする

Googleでログイン後のリダイレクト先:

https://127.0.0.1/callback?code=4%2F0A**********ALg&scope=profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=1&prompt=consent

(3) トークンエンドポイントへのアクセス

Webサーバ側でGoogleのトークンエンドポイントと通信します。

リクエスト:

curl -v -X POST \
-d "client_id=929**********bro.apps.googleusercontent.com" \
-d "client_secret=***********************************" \
-d "redirect_uri=https://127.0.0.1/callback" \
-d "grant_type=authorization_code" \
-d "code=4%2F0A**********ALg" \
https://www.googleapis.com/oauth2/v4/token

レスポンス:

{
  "access_token": "ya29.A0**********63",
  "expires_in": 3599,
  "scope": "https://www.googleapis.com/auth/userinfo.profile openid",
  "token_type": "Bearer",
  "id_token": "eyJ**********ifQ.eyJ**********DF9.bui**********7Hg"
}

(4) JWT解析

トークンエンドポイントのレスポンスのid_tokenはJWT形式なので、Base64デコードして内容を検証します。

{
  "alg": "RS256",
  "kid": "fda1066453dc9dc3dd933a41ea57da3ef242b0f7",
  "typ": "JWT"
}
{
  "iss": "https://accounts.google.com",
  "azp": "929**********bro.apps.googleusercontent.com",
  "aud": "929**********bro.apps.googleusercontent.com",
  "sub": "104650147220769694403",
  "at_hash": "nw7QKWVVCnkxWvhgYnpi8A",
  "nonce": "4567",
  "name": "tex2e",
  "picture": "https://lh3.googleusercontent.com/a-/AFdZucpyZ9viFBC0DmLcdDYiXj78GpmnTwSRLKKjrb2_=s96-c",
  "given_name": "tex2e",
  "locale": "ja",
  "iat": 1660020881,
  "exp": 1660024481
}
IDトークンの署名データ

IDトークン (id_token) のJWTには以下の情報が含まれています。

  • sub : エンドユーザを識別するためのID (subject)
  • iat : 発行日時 (issued at)
  • exp : 有効期限 (expiration time)
  • nonce : 認証リクエストに含まれるnonceと同じ値が含まれる。乱数を入れることでリプレイ攻撃を防ぐことができる
  • aud : リライングパーティのクライアントID (audience)
  • iss : IDトークンの発行者 (issuer)
  • IDトークンの署名データ(リライングパーティがIDトークンの署名を検証することで、IDトークン入れ替え攻撃を防ぐことができる)

(5) UserInfoエンドポイントへのアクセス

取得したアクセストークンを利用して、ユーザ情報を取得します。

curl \
-H 'Authorization: Bearer ya29.A0**********63' \
https://openidconnect.googleapis.com/v1/userinfo

レスポンス:

{
  "sub": "104650147220769694403",
  "name": "tex2e",
  "given_name": "tex2e",
  "email": "tex2eq@gmail.com",
  "email_verified": true,
  "picture": "https://lh3.googleusercontent.com/a-/AFdZucpyZ9viFBC0DmLcdDYiXj78GpmnTwSRLKKjrb2_\u003ds96-c",
  "locale": "ja"
}

取得した情報をブラウザで表示させます。 pictureのURLは一定時間経過すると400エラーになる点に注意が必要です。

Googleログイン画面

OpenID Connectの検証に使用したWebページの実装をGitHubに上げましたので、必要に応じて参照ください。

tex2e/openid-connect-example: Google OAuth Example (For Study Purpose Only)

以上です。

参考文献

  • https://developers.google.com/identity/protocols/oauth2/openid-connect
  • Auth屋『OAuth、OAuth認証、OpenID Connectの違いを整理して理解できる本』