Flaskを使用したFacebook ログイン 連携用API作成 メモ



* Flask を使用してFacebookログイン連携するためのAPI作成方法についてメモする。
  * [Google連携用API](https://qiita.com/KWS_0901/items/95ce6e28e44f14c38545)を試した時と同様に、Docker起動できる形で[過去に作成したFacebook検証コード](https://qiita.com/KWS_0901/items/e9b0f81464370ffed3cd)を部分的にAPI化した。

## 作成するAPI

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

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

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

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

  * リクエスト例

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

  * レスポンス例

    ```json
    {
        "authorization_request_url": "https://www.facebook.com/v10.0/dialog/oauth?client_id=AAABBBBCCC&scope=email&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fcallback&state=ABCDE12345&response_type=code",
        "state": "ABCDE12345"
    }
    ```


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

* 認可レスポンスとして受け取った認可コード(`code`)を指定して、トークンリクエスト＋ユーザー情報取得を行う。

  * 今回は簡略化のため認可リクエストパラメータのDB保存まで実装しておらず、検証処理は行っていないが、**stateパラメータの検証は必ず行うこと。**

  * リクエスト

    ```http
    POST /api/facebook/auth_request/complete HTTP/1.1
    Host: localhost:5000
    Content-Type: application/json
    Content-Length: 363
    
    {
        "code":"XXX"
    }
    ```

* レスポンス例

  ```json
    {
        "id": "1234567890",
        "name": "山田太郎"
    }
  ```

  

## プロジェクト構成

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

## 実装

* `docker-compose.yml`

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

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

* `facebook.env`

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

  ```env
  FACEBOOK_CLIENT_ID=YOUR_CLIENT_ID
  FACEBOOK_CLIENT_SECRET=YOUR_CLIENT_SECRET
  FACEBOOK_SCOPE_LIST=YOUR_SCOPE_LIST
  FACEBOOK_REDIRECT_URI=YOUR_REDIRECT_URI
  FACEBOOK_AUTHORIZATION_ENDPOINT=https://www.facebook.com/v10.0/dialog/oauth
  FACEBOOK_TOKEN_ENDPOINT=https://graph.facebook.com/v10.0/oauth/access_token
  FACEBOOK_APP_TOKEN_ENDPOINT=https://graph.facebook.com/oauth/access_token
  FACEBOOK_TOKEN_VALIDATION_ENDPOINT=https://graph.facebook.com/debug_token
  FACEBOOK_USER_INFO_ENDPOINT=https://graph.facebook.com/v10.0/
  ```

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

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

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

  * コントローラー

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

    ```python
    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
    from pprint import pprint
    
    # Routing Settings
    facebook_router = Blueprint('facebook_router', __name__)
    
    # Client Param
    client_id = os.getenv('FACEBOOK_CLIENT_ID')
    client_secret = os.getenv('FACEBOOK_CLIENT_SECRET')
    redirect_uri = os.getenv('FACEBOOK_REDIRECT_URI')
    scope_list = os.getenv('FACEBOOK_SCOPE_LIST')
    
    # Facebook Endpoint
    authorization_endpoint = os.getenv('FACEBOOK_AUTHORIZATION_ENDPOINT')
    token_endpoint = os.getenv('FACEBOOK_TOKEN_ENDPOINT')
    app_token_endpoint = os.getenv('FACEBOOK_APP_TOKEN_ENDPOINT')
    token_validation_endpoint = os.getenv('FACEBOOK_TOKEN_VALIDATION_ENDPOINT')
    user_info_endpoint = os.getenv('FACEBOOK_USER_INFO_ENDPOINT')
    
    app = Flask(__name__)
    
    
    # Create Authorization Request Endpoint
    @facebook_router.route("/facebook/auth_request/create",  methods=['POST'])
    def create():
        # Generate state
        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,
            '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
    @facebook_router.route("/facebook/auth_request/complete", methods=['POST'])
    def complete():
        # Parse Req Body
        jsonData = json.dumps(request.json)
        req_body = json.loads(jsonData)
        # Exchange Authorization code for Access Token
        token_req_url = token_endpoint+'?{}'.format(parse.urlencode({
            'client_id': client_id,
            'client_secret': client_secret,
            'redirect_uri': redirect_uri,
            'code': req_body["code"]
        }))
        try:
            token_req = req.Request(token_req_url, method='GET')
            with req.urlopen(token_req) as token_res:
                token_res_body = token_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)
        access_token = json.loads(token_res_body)['access_token']
    
        # Generate App Access Token
        # https://developers.facebook.com/docs/facebook-login/access-tokens/#apptokens
        app_token_req_url = app_token_endpoint+'?{}'.format(parse.urlencode({
            'client_id': client_id,
            'client_secret': client_secret,
            'grant_type': 'client_credentials'
        }))
        try:
            app_token_req = req.Request(app_token_req_url, method='GET')
            with req.urlopen(app_token_req) as app_token_res:
                app_token_res_body = app_token_res.read()
        except error.HTTPError as err:
            err_str = str(err.code) + ':' + err.reason + ':' + str(err.read())
        except error.URLError as err:
            err_str = err.reason
        app_token = json.loads(app_token_res_body)['access_token']
    
        # Validate Access Token
        token_validation_req_url = token_validation_endpoint+'?{}'.format(parse.urlencode({
            'input_token': access_token,
            'access_token': app_token
        }))
        try:
            token_validation_req = req.Request(
                token_validation_req_url, method='GET')
            with req.urlopen(token_validation_req) as token_validation_res:
                token_validation_res_body = token_validation_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)
        # Get User Info
        # https://developers.facebook.com/docs/graph-api/reference/user
        user_request_url = user_info_endpoint+'/' + \
        json.loads(token_validation_res_body)['data']['user_id']+'?fields=email&access_token='+access_token

          user_req = req.Request(user_request_url, method='GET')
        with req.urlopen(user_req) as user_res:
            user_res_body = user_res.read()
    
        return json.loads(user_res_body)
    
    ```

    

## 起動

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



## 参考情報

* [ログインフローを手動で構築する](https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow?locale=ja_JP)
* [アクセストークン](https://developers.facebook.com/docs/facebook-login/access-tokens/#apptokens)