PythonでGoogle Sign-Inを使用して認証の実装

はじめに

サービスを作るときに、一部のユーザーのみ提供したい場合があるでしょう。

社内で、ビジネスを加速させるために、データサイエンスツールを作っていた。社内限定なツールなので、アクセス制限が必要です

弊社は全員Gmail使用するため、ユーザーシステムを持たずに、Google Sign-In経由、手軽に認証システムを作りました。

本文は、Google Sign-Inを使用するユーザー認証のプロセスを紹介します。構成は以下です:

  • OAuth2とGoogleログインの紹介
  • Google Authを用いた認証のプロセス
  • FlaskでGoogleログインを実装

Talk is cheap, show me the code.」を信じている人は、「Google Sign-Inを用いた認証のプロセス」の部分だけ見れば良い。

自分の理解が限られるなので、もし不適切なところがありましたら、ご指摘いただけると幸いです。

OAuthとGoogleログインの紹介

OAuthが広く使われている概念ですが、自体はややこしいなので、誤解されていることがあるでしょう。先に関連する概念を説明します。

基本概念

認証(Authentication、AuthN):通信の相手が誰(何)であるかを確認すること。

認可(Authorization、AuthZ):とある特定の条件に対して、リソースアクセスの権限を与えること。

なので、認可には、「誰」という概念はなく、「リソース」という概念が必要。リソースに対してアクセス可否を判断することです。 リソースの定義は略、WebページやAPIやなどのことです。

OAuth:権限の認可を行うためのプロトコルである。現時点の標準は2.0版なので、OAuth2と呼ばれている。

OpenID Connect:OAuth 2.0 プロトコルの上にシンプルなアイデンティティレイヤーを付与したものである。

簡単に言うと

OpenID Connect = OAuth2 + Identity Layer

Google Sign-InGoogleアカウントでサインイン、軽減する認証システムです。Googleユーザーと連結することもできます。

Google Sign-Inは、OAuth 2.0のプロトコルに従っています。

出典:https://developers.google.com/identity?hl=ja

OAuth2認可のプロセス

OAuth2に関する4つのロール

Resource Owner:リソースへのアクセス権限を与えられる人またはエンティティ。例えば、ユーザー。

Resource Server:リソースを格納するかつAccessTokenを使ったリソースリクエストに応えられるサーバー。例えば、FacebookGoogleGithubAPIサーバー。

Client:Resource Ownerの代理として、リソースを要求するアプリケーション。例えば、Kaggle(Facebookでログインできます)。

Authorization Server:例えば、FacebookGoogleGithubの認証サーバー。

OAuth2認可プロセス

OAuth2における、認可の流れは以下です:

     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

出典:https://tools.ietf.org/html/rfc6749

簡単に説明すると、

  1. ClientはResouce Ownerに特定リソースの権限を要求する、Resouce Ownerは権限付与を同意(Authorization Grantを発行)する
  2. ClientはAuthorization GrantをAuthorization Serverに見せて、AccessTokenを要求する。Authorization Serverは検証し、AccessTokenをClientに渡します。
  3. ClientはAccessTokenを使って、Resource Serverにリソースを要求する、Resource ServerはAccessTokenを検証し、リクエストの結果を返す。

具体出来に、どう検証するのか、GoogleにおけるOAuth2の認可のプロセスの部分に参照してください。

承認のフローがややこしいなので、詳細は

OAuth 2.0 全フローの図解と動画 - Qiita

に参考してください。

Open Id Connect のプロセス

Open Id Connectの流れは以下です、説明が略。

+--------+                                   +--------+
|        |                                   |        |
|        |---------(1) AuthN Request-------->|        |
|        |                                   |        |
|        |  +--------+                       |        |
|        |  |        |                       |        |
|        |  |  End-  |<--(2) AuthN & AuthZ-->|        |
|        |  |  User  |                       |        |
|   RP   |  |        |                       |   OP   |
|        |  +--------+                       |        |
|        |                                   |        |
|        |<--------(3) AuthN Response--------|        |
|        |                                   |        |
|        |---------(4) UserInfo Request----->|        |
|        |                                   |        |
|        |<--------(5) UserInfo Response-----|        |
|        |                                   |        |
+--------+                                   +--------+

出典: https://openid.net/specs/openid-connect-core-1_0.html

なぜOAuth2が必要なのか

下記の仮想ユースケースから説明します。

ユーザーAは、Googleアカウントを持っています。そして、幾つのリソースが持っています:

  • メールアドレス
  • アイコン
  • 年齢情報
  • など

ユーザーが自分のユーザー名とパースワードでログインするから、リソースの訪問することができます。

ユーザーAは、あるアプリBで、活動するために、Googleアカウントの特定情報(例えばアイコン)が必要です。

アプリBが、ユーザーAのGoogleアカウントの特定のリソースをアクセスすることが必要となっています。

しかし、このリソースはプラベートなので、A以外は誰でも訪問出来ない。Aは、リソース訪問権限をBに付与したい場合は、どうすればいいでしょうか?

方法1:ユーザーAはアカウントとパスワードをBに渡し、Bはリソースをアクセスする。

リソースのアクセスができますが、以下のデメリットがあります:

  1. 安全ではありません:パースワードを渡すのは怖いでしょう
  2. 権限が大きい:Bが認証を取得すると、Aのすべてのリソースにアクセスできる
  3. 簡単にキャンセルできない:パースワードを変更する以外は認証を失効されることはできない

方法2:ユーザーAが一定期間内、特定リソースへアクセスする「トークン」をBに渡す、Bはこの「トークン」を持って、リソースを訪問します。

パスワードより、トークンは以下のメリットがあります:

  1. パスワードが漏らない
  2. 特定のサービスに限定できる
  3. 期間を過ぎたら失効出来る

Access Tokenの理解

お客さん、会社に見学することをイメージして。

お客さんの資格を審査し、お客さんシールと食券を渡します。社員のセキュリティカードと違うものです。

お客さんはシールを使って、会社の特定なエリアで見学できますし、食券を使って食事もできます、でもそれ以外のコンフィデンシャルのところの立ち入ることはできない。

見学終了あと、シールと食券の使用はできなくなて、再入場の場合、再度の審査とシールの再発行が必要です。

トークンは似てるなものです。

Google Sign-Inを用いた認証のプロセス

Google Sign-In」での認証実際は、GoogleのOAuth2サービスを使って、ユーザーからGoogleアカウント情報をアクセスの認可をもらえて、ユーザーのGoogleアカウントの情報を使って、ユーザーをウェブサイトの訪問を許すかどうか判断することです。

GoogleにおけるOAuth2の認可のプロセス

OAuth2認証のプロセスに従って、GoogleSignInのプロセスを説明します。

権限を要求する

ユーザーがログインボタンを押すとき、ClientはGoogle Login画面をリダイレクトして、権限を要求する。ユーザーを同意すると、権限を付与されます。

権限付与のとき、(事前発行した)client_idや、同意するscopeをGoogleに渡します。

f:id:hirogen317:20200118203617p:plain
GoogleLogin

AccessTokenを要求する

ユーザーが同意する後、GoogleはClientを事前登録したCallbackのUriを叩いて、(Grant)codeを渡します。 Clientはそのcodeとclient_id, client_secretを使って、AuthAPIを叩き、AccessTokenを請求します。 Google側が検証して、AccessTokenをResponseで返します。

リソースを請求する

一定期間内、返したAccessTokenを使って、ユーザーのリソースをアクセス出来ます。

PythonGoogle Sign-Inで認証の実装

事前準備

ウェブアプリはGoogleAuthを登録して、以下の情報をもらえます:

  • client_id
  • client_secret

そして、以下の情報を、Googleに登録します:

  • domain
  • Redirect Uri

準備の詳細は、このこのページに参考してください:

https://developers.google.com/identity/protocols/OpenIDConnect?hl=ja

FlaskでGoogleログインを実装する

最低限の機能を実現するコードを実装してみます。

フォルダストラクチャー:

-- requirements.txt
-- index.py
-- templates
    |-- login.html
    |-- index.html

index.py

ポイントはcallback関数。 Google Sign-Inは、callback関数のURIを叩いて、コードを返します。

from flask import Flask, render_template, request
import requests
import logging


logging.basicConfig(level=logging.DEBUG)


app = Flask(__name__)


@app.route('/login')
def login():
    return render_template("login.html")


@app.route('/oauth/redirect')
def callback():
    client_id = "your_client_id"
    client_secret = "your_client_secret"
    code = request.args["code"]
    url = "https://www.googleapis.com/oauth2/v4/token"

    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    form = dict(code=code,client_id=client_id,  client_secret=client_secret,redirect_uri='http://localhost:5000/oauth/redirect',grant_type='authorization_code')
    # codeとclient_secretと合わせて、AccessTokenを請求
    resp = requests.post(url, headers=headers, data=form)
    # access_tokenを取得
    access_token = resp.json()['access_token']
    # access_tokenを使ってリソースを獲得
    resp = requests.get('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={access_token}'.format(access_token=access_token))
    logging.info(resp.json())
    email=resp.json()['email']
    # validate the email, if on whitelist, etc.
    return render_template("index.html", email=email)

if __name__ == '__main__':
    app.run()

templates/login.html

ポイントは渡すのパラメタで、client_id、redirect_uri、scopeのところ、適当に切り替えすることです。 scopeの設定について、Googleのドキュメントに参照してください:

https://developers.google.com/identity/protocols/OpenIDConnect?hl=ja#scope-param

<h1>hello</h1>

<a href="https://accounts.google.com/o/oauth2/auth?client_id=your_client_id&redirect_uri=http://localhost:5000/oauth/redirect&response_type=code&scope=openid%20email">グーグルで認証</a>

templates/index.html

<h1>hello</h1>

{{email}}

引用

OAuth - Wikipedia

Final: OpenID Connect Core 1.0 incorporating errata set 1

OAuth 2.0の代表的な利用パターンを仕様から理解しよう - Build Insider

OpenID Connectユースケース、OAuth 2.0の違い・共通点まとめ - Build Insider

OpenID Connect 全フロー解説 - Qiita

よくわかる認証と認可 | Developers.IO

Using OAuth 2.0 to Access Google APIs  |  Google Identity Platform

OpenID Connect  |  Google Identity Platform  |  Google Developers

Final: OpenID Connect Core 1.0 incorporating errata set 1

OAuth 2.0 の仕様を読むために - Qiita