Flaskを使用したZoom OAuth 連携用API作成 メモ

* Flask を使用してZoom OAuth連携をサポートするためのAPI作成方法についての個人用メモ。
  * [Google連携用API](https://qiita.com/KWS_0901/items/95ce6e28e44f14c38545)を試した時と同様に、Docker起動できる形で部分的にAPI化した。



## 作成するAPI

### ①認可リクエスト作成API

* Zoomへの認可エンドポイントへアクセスするためのURLをパラメータをつけて生成・返却する。

  * `client_id`など固定の属性値は環境変数から取得する。

  * `state`はAPI呼び出し時に生成し、レスポンスとして返却する。

  * リクエスト例

    ```http
    POST /api/zoom/auth_request/create HTTP/1.1
    Host: localhost:5000
    ```

  * レスポンス例

    ```json
    {
      "authorization_request_url": "https://zoom.us/oauth/authorize?client_id=...&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fzoom%2Fcallback&state=...&response_type=code",
      "state":"3708b06366830c67818c1d441c5964fa9ac3f0340f4690c3249b8d42d8610855"
}
    ```


### ②トークンリクエスト+ユーザー情報取得API

* 認可レスポンスとしてzoomからのリダイレクトで受け取った認可コードを指定し、トークンリクエストとユーザー情報取得を行う。
  * 今回は簡略化のため認可リクエストパラメータのDB保存まで実装しておらず、検証処理は行っていない。実運用では検証処理を入れることを推奨。
  * リクエスト例

    ```http
    POST /api/zoom/auth_request/complete HTTP/1.1
    Host: localhost:5000
    Content-Type: application/json
    Content-Length: 52
    
    {
        "code":"..."
    }
    ```

  * レスポンス例(ユーザー情報)

    ```json
    {
        "account_id": "...",
        "cms_user_id": "",
        "created_at": "2021-07-03T08:32:58Z",
        "dept": "",
        "email": "hogehoge@example.com",
        "first_name": "...",
        "group_ids": [],
        "host_key": "...",
        "id": "...",
        "im_group_ids": [],
        "jid": "....",
        "job_title": "",
        "language": "jp-JP",
        "last_login_time": "2021-07-03T08:32:58Z",
        "last_name": "...",
        "location": "",
        "login_types": [
            1,
            100
        ],
        "personal_meeting_url": "...",
        "phone_country": "",
        "phone_number": "",
        "pic_url": "...",
        "pmi": ...,
        "role_id": "0",
        "role_name": "Owner",
        "status": "active",
        "timezone": "",
        "type": 1,
        "use_pmi": false,
        "verified": 0
    }
    ```

    

## 事前作業

* [アプリケーション登録](https://marketplace.zoom.us/docs/guides/build/oauth-app)を行い、クライアントID/シークレットの発行、リダイレクトURIの登録を行う。

## プロジェクト構成

```
facebook_oidc
└─ docker-compose.yml
└─ zoom.env
│
└─ be
   └─ Dockerfile
   └─ requirements.txt
   │
   └─app
      └─ app.py
      │
      └─ api
          └─ __init__.py
          │
          └─views
             └─  zoom.py
```

## 実装

* `docker-compose.yml`

  ※起動時に環境変数ファイル`zoom.env`を読み込む。

  ```yaml
  version: "3"
  services:
    be:
      container_name: be
      build: ./be
      env_file: zoom.env
      volumes:
        - ./be/app:/app
      ports:
        - "5000:5000"
      command: flask run --host 0.0.0.0 --port 5000
      tty: true
  ```

* `zoom.env`

  * 環境変数ファイル。アプリケーション登録時に発行・設定した値を指定する。

  ```env
  CLIENT_ID=YOUR_CLIENT_ID
  CLIENT_SECRET=YOUR_CLIENT_SECRET
  REDIRECT_URI=http:YOUR_CALLBACK_URL
  AUTHORIZATION_ENDPOINT=https://zoom.us/oauth/authorize
  TOKEN_ENDPOINT=https://zoom.us/oauth/token
  USERINFO_ENDPOINT=https://zoom.us/v2/users/me
  ```

* `be/requirements.txt`

  * Pythonライブラリ一式

  ```
  Flask
  Flask-Cors
  ```

* `be/Dockerfile`

  ```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`

  ```python
  from api import app
  
  if __name__ == '__main__':
      app.run()
  ```

* `be/api/__init__.py`

  ```python
  from flask import Flask
  from .views.zoom import zoom_router
  from flask_cors import CORS
  
  
  def create_app():
  
      app = Flask(__name__)
  
      CORS(app, supports_credentials=True)
  
      app.register_blueprint(zoom_router, url_prefix='/api')
  
      return app
  
  
  app = create_app()
  
  ```

* `be/api/views/zoom.py`

  * コントローラー

  * ※エラーハンドリング皆無

    ```python
    

```python
from flask import Flask, Blueprint, request
import hashlib
import urllib.parse as parse
import urllib.request as req
import urllib.error as error
import json
import os
from pprint import pprint

# Routing Settings
zoom_router = Blueprint('zoom_router', __name__)

# Client Param
client_id = os.getenv('CLIENT_ID')
client_secret = os.getenv('CLIENT_SECRET')
redirect_uri = os.getenv('REDIRECT_URI')

# Zoom Endpoint
authorization_endpoint = os.getenv('AUTHORIZATION_ENDPOINT')
token_endpoint = os.getenv('TOKEN_ENDPOINT')
userinfo_endpoint = os.getenv('USERINFO_ENDPOINT')

app = Flask(__name__)


# Create Authorization Request Endpoint
@zoom_router.route("/zoom/auth_request/create",  methods=['POST'])
def create():

    state = hashlib.sha256(os.urandom(32)).hexdigest()
    # Create Authz Request URL
    # https://zoom.us/oauth/authorize?response_type=code&client_id=...&redirect_uri=...&state=...
    auth_request_url = authorization_endpoint+'?{}'.format(parse.urlencode({
        'client_id': client_id,
        'redirect_uri': redirect_uri,
        'state': state,
        'response_type': 'code'
    }))
    res_body = {
        "authorization_request_url": auth_request_url,
        "state": state
    }
    return json.loads(json.dumps(res_body))


# Token Request And Get User Info Endpoint
@zoom_router.route("/zoom/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)
    access_token = json.loads(token_res)['access_token']
    # Get User Info
    # https://marketplace.zoom.us/docs/api-reference/zoom-api/users/user
    headers = {
        'Authorization': 'Bearer ' + access_token
    }
    user_info_req = req.Request(
        userinfo_endpoint, headers=headers, method='GET')
    try:
        with req.urlopen(user_info_req) as user_info_res:
            user_info_res_body = user_info_res.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)
    return json.loads(user_info_res_body)
```

    

## 起動

```shell
docker-compose up -d
```





## 参考情報

* [ウェブアプリにzoomログインを組み込む（zoomログイン v2.0）](https://developers.zoom.biz/ja/docs/zoom-login/integrate-zoom-login-v2/)

* [Create an OAuth App](https://marketplace.zoom.us/docs/guides/build/oauth-app)
* [Zoom API](https://marketplace.zoom.us/docs/api-reference/zoom-api)
* [Zoomのアプリケーションの作成](https://1507t.xyz/zoom-app-create)
* [OAuth with Zoom](https://marketplace.zoom.us/docs/guides/auth/oauth)