3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Flask、PyJWTを使用したGoogle OpenID 連携用API作成 メモ

Posted at
  • Flask とPyJWTを使用してGoogleとのOpenID連携するための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

参考情報

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?