- Flask とPyJWTを使用したGoogleとのOpenID連携方法についてメモする。
大まかな処理の流れ
-
Googleへ認可リクエスト(Authorization Request)を行う。
-
ユーザー認証/同意を行い、認可レスポンスを受け取る。
-
認可レスポンスを使ってトークンリクエストを行う。
事前準備
-
Googleデベロッパーコンソールからプロジェクト、クライアントを登録する。
- アプリケーションの種類
* ウェブアプリケーション - クライアントID/シークレット発行
- スコープ設定
- リダイレクトURI設定
* 今回はhttp://localhost:5000/callback
を設定
- アプリケーションの種類
-
Pythonライブラリインストール
-
flask
* ウェブアプリ開発用フレームワーク -
pyjwt
* JSON Web Tokenエンコード/デコード用ライブラリ
* id_token検証に使用する。
-
-
id_token署名情報確認
-
jwks_uri からJWKを確認する。
-
id_tokenのヘッダー記載のkidと同じJWKを確認する(下記
jwk_json
)。 -
JWKに対してpyjwtを使用して公開鍵を取得する。
from jwt.algorithms import RSAAlgorithm jwk_json = { "e": "AQAB", "use": "sig", "n": "q_GoX7XASWstA7CZs3acUgCVB2QhwhupF1WZsIr6FoI-DpLaiTlGLzEJlkLKW2nthUP35lqhXilaInOAN86sOEssz4h_uEycVpM_xLBRR-7Rqs5iXype340JV4pNzruXX5Z_Q4D7YLvm2E1QWivvTK4FiSCeBbo78Lpkr5atiHmWEcLENoquhEHdpij3wppdDlL5eUAy4xH6Ait5IDe66RehBEGfs3MLnCKyGAPIammSUruV0BEmUPfecLoXNhpuAfoGs3TO-5CIt1jmaRL2B-A2UxhPQkpE4Q-U6OJ81i4nzs34dtaQhFfT9pZqkgOwIJ4Djj7HI1xKOmoExMCDLw", "kid": "774573218c6f6a2fe50e29acbc686432863fc9c3", "kty": "RSA", "alg": "RS256" } public_key = RSAAlgorithm.from_jwk(jwk_json)
-
実装
- 認可リクエスト ~ トークンリクエスト(id_token検証含む)までのコード
import hashlib
from flask import Flask, request, redirect, session
import urllib.request
import urllib.parse
import json
import base64
from pprint import pprint
import os
import jwt
from jwt.algorithms import RSAAlgorithm
# id_token 検証用公開鍵
# https://www.googleapis.com/oauth2/v3/certs
jwk_json = {
"e": "AQAB",
"use": "sig",
"n": "q_GoX7XASWstA7CZs3acUgCVB2QhwhupF1WZsIr6FoI-DpLaiTlGLzEJlkLKW2nthUP35lqhXilaInOAN86sOEssz4h_uEycVpM_xLBRR-7Rqs5iXype340JV4pNzruXX5Z_Q4D7YLvm2E1QWivvTK4FiSCeBbo78Lpkr5atiHmWEcLENoquhEHdpij3wppdDlL5eUAy4xH6Ait5IDe66RehBEGfs3MLnCKyGAPIammSUruV0BEmUPfecLoXNhpuAfoGs3TO-5CIt1jmaRL2B-A2UxhPQkpE4Q-U6OJ81i4nzs34dtaQhFfT9pZqkgOwIJ4Djj7HI1xKOmoExMCDLw",
"kid": "774573218c6f6a2fe50e29acbc686432863fc9c3",
"kty": "RSA",
"alg": "RS256"
}
public_key = RSAAlgorithm.from_jwk(jwk_json)
# iss(トークン発行者)
issuer = 'https://accounts.google.com'
# クライアント情報
# 自身の環境に合わせて設定する。
client_id = <YOUR_CLIENT_ID>
client_secret = <YOUR_CLIENT_SECRET>
redirect_uri = 'http://localhost:5000/callback'
# Google エンドポイント
authorization_base_url = 'https://accounts.google.com/o/oauth2/v2/auth'
token_url = 'https://www.googleapis.com/oauth2/v4/token'
app = Flask(__name__)
app.secret_key = 'session_key'
# 認可リクエスト用
@app.route("/login")
def login():
# nonce、stateの生成と保存
nonce = hashlib.sha256(os.urandom(32)).hexdigest()
state = hashlib.sha256(os.urandom(32)).hexdigest()
session['nonce'] = nonce
session['state'] = state
# Googleへの認可リクエスト
return redirect(authorization_base_url+'?{}'.format(urllib.parse.urlencode({
'client_id': client_id,
'scope': 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
'redirect_uri': redirect_uri,
'state': state,
'nonce': nonce,
'response_type': 'code'
})))
# 認可レスポンス用リダイレクションエンドポイント + トークンリクエスト用
@app.route("/callback")
def callback():
# state 検証
state = request.args.get('state')
if state != session['state']:
print("invalid_redirect")
code = request.args.get('code')
# トークンリクエスト
body = urllib.parse.urlencode({
'code': code,
'client_id': client_id,
'client_secret': client_secret,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code'
}).encode('utf-8')
req = urllib.request.Request(token_url)
with urllib.request.urlopen(req, data=body) as f:
res = f.read()
# id_token 検証
content = json.loads(res)
id_token = content['id_token']
claims = jwt.decode(id_token,
public_key,
issuer=issuer,
audience=client_id,
algorithms=["RS256"])
# nonce 検証
if claims['nonce'] != session['nonce']:
return "invalid id_token"
return claims
if __name__ == "__main__":
app.run(debug=True)