- Flask とPyJWTを使用してGoogleとのOpenID連携するためのAPI作成方法についてメモする。
- 連携処理をバックエンド側に持たせたかったため、Docker起動できる形で過去に作成した検証コードをAPI化した。
作成するAPI
認可リクエスト作成API
-
Googleへの認可エンドポイントへアクセスするためのURLをパラメータをつけて生成・返却する。
-
state
,nonce
はAPI呼び出し時に生成し、レスポンスとして返却する。 -
client_id
など固定の属性値は環境変数から取得する。 -
リクエスト例
POST /api/google/auth_request/create HTTP/1.1 Host: localhost:5000
-
レスポンス例
{ "authorization_request_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=ABC12345&scope=hoge&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fcallback&state=ABCDE12345&nonce=FGHIJ67890&response_type=code", "nonce": "FGHIJ67890", "state": "ABCDE12345" }
-
トークンリクエスト+ユーザー情報取得API
-
認可レスポンスとして受け取った認可コードを指定して、トークンリクエスト+id_token検証(ユーザー情報取得)を行う。
-
今回は簡略化のため認可リクエストパラメータのDB保存までは実装しておらず、検証処理は行っていないが、
state
パラメータの検証は必須。 -
リクエスト例
POST /api/google/auth_request/complete HTTP/1.1 Host: localhost:5000 Content-Type: application/json Content-Length: 173 { "code":"XXXXX", "nonce":"FGHIJ67890" }
-
レスポンス例
{ "email": "test@example.com", "email_verified": true, "sub": "123456789123456789" }
-
プロジェクト構成
google_oidc
└─ docker-compose.yml
└─ google.env
│
└─ be
└─ Dockerfile
└─ requirements.txt
│
└─app
└─ app.py
│
└─ api
└─ __init__.py
│
└─views
└─ google.py
実装
-
docker-compose.yml
※起動時に環境変数ファイル
google.env
を読み込む。version: "3" services: be: container_name: be build: ./be env_file: google.env volumes: - ./be/app:/app ports: - "5000:5000" command: flask run --host 0.0.0.0 --port 5000 tty: true
-
google.env
- 環境変数ファイル。アプリケーション登録時に発行・設定した値を指定する。
CLIENT_ID=YOUR_CLIENT_ID CLIENT_SECRET=YOUR_CLIENT_SECRET REDIRECT_URI=YOUR_REDIRECT_URI AUTHORIZATION_ENDPOINT=https://accounts.google.com/o/oauth2/v2/auth TOKEN_ENDPOINT=https://www.googleapis.com/oauth2/v4/token SCOPE_LIST=YOUR_SCOPE_LIST ISSUER=https://accounts.google.com
-
be/requirements.txt
- Pythonライブラリ一式
Flask Flask-Cors PyJWT cryptography
-
be/Dockerfile
FROM python:3.8 RUN mkdir /app ADD requirements.txt /app ENV PYTHONUNBUFFERED 1 EXPOSE 5000 WORKDIR /app RUN pip3 install -r requirements.txt
-
be/app/app.py
from api import app if __name__ == '__main__': app.run()
-
be/api/__init__.py
from flask import Flask from .views.google import google_router from flask_cors import CORS def create_app(): app = Flask(__name__) CORS(app, supports_credentials=True) app.register_blueprint(google_router, url_prefix='/api') return app app = create_app()
-
be/api/views/google.py
-
コントローラー
-
id_token検証用の署名情報は、事前にid_tokeの形式を確認し、コード中にべた書きしている。
import hashlib from flask import Flask, Blueprint, request import urllib.parse as parse import urllib.request as req import urllib.error as error import json import os import jwt from jwt.algorithms import RSAAlgorithm from pprint import pprint # Routing Settings google_router = Blueprint('google_router', __name__) # Client Param client_id = os.getenv('CLIENT_ID') client_secret = os.getenv('CLIENT_SECRET') redirect_uri = os.getenv('REDIRECT_URI') scope_list = os.getenv('SCOPE_LIST') # Google Endpoint authorization_endpoint = os.getenv('AUTHORIZATION_ENDPOINT') token_endpoint = os.getenv('TOKEN_ENDPOINT') # id_token Validation Param issuer = os.getenv('ISSUER') # 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) app = Flask(__name__) # Create Authorization Request Endpoint @google_router.route("/google/auth_request/create", methods=['POST']) def create(): # Generate nonce and state nonce = hashlib.sha256(os.urandom(32)).hexdigest() state = hashlib.sha256(os.urandom(32)).hexdigest() # Create Authz Request URL auth_request_url = authorization_endpoint+'?{}'.format(parse.urlencode({ 'client_id': client_id, 'scope': scope_list, 'redirect_uri': redirect_uri, 'state': state, 'nonce': nonce, 'response_type': 'code' })) res_body = { "authorization_request_url": auth_request_url, "state": state, "nonce": nonce } return json.loads(json.dumps(res_body)) # Token Request And Get User Info Endpoint @google_router.route("/google/auth_request/complete", methods=['POST']) def complete(): # Parse Req Body jsonData = json.dumps(request.json) req_body = json.loads(jsonData) # Token Request token_req_body = parse.urlencode({ 'code': req_body["code"], 'client_id': client_id, 'client_secret': client_secret, 'redirect_uri': redirect_uri, 'grant_type': 'authorization_code' }).encode('utf-8') token_req = req.Request(token_endpoint) err_str = '' try: with req.urlopen(token_req, data=token_req_body) as f: token_res = f.read() except error.HTTPError as err: err_str = str(err.code) + ':' + err.reason + ':' + str(err.read()) pprint(err_str) except error.URLError as err: err_str = err.reason pprint(err_str) # id_token Validation by PyJWT id_token = json.loads(token_res)['id_token'] claims = jwt.decode(id_token, public_key, nonce=req_body['nonce'], issuer=issuer, audience=client_id, algorithms=["RS256"]) # nonce Validation if claims['nonce'] != req_body['nonce']: return "invalid id_token" res_body = { "sub": claims["sub"], "email": claims["email"], "email_verified": claims["email_verified"] } return json.loads(json.dumps(res_body))
-
起動
docker-compose up -d