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

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

## 作成するAPI

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

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

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

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

  * リクエスト例

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

  * レスポンス例

    ```json
    {
        "authorization_request_url": "https://access.line.me/dialog/oauth/weblogin?client_id=123456789&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fcallback&state=ABCDE12345&response_type=code",
        "state": "ABCDE12345"
    }
    ```


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

* 認可レスポンスとしてLINEからのリダイレクトで受け取った認可コードを指定し、トークンリクエスト＋ユーザー情報取得を行う。

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

  * リクエスト例

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

  * レスポンス例

    ```json
    {
            "displayName": "山田太郎",
            "userId": "ABCDE12345"
    }
    ```

    

  

## プロジェクト構成

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

## 実装

* `docker-compose.yml`

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

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

* `line.env`

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

  ```env
  LINE_CLIENT_ID=YOUR_CHANNEL_ID
  LINE_CLIENT_SECRET=YOUR_CHANNEL_SECRET
  LINE_REDIRECT_URI=YOUR_CALLBACK_URL
  LINE_AUTHORIZATION_ENDPOINT=https://access.line.me/dialog/oauth/weblogin
  LINE_TOKEN_ENDPOINT=https://api.line.me/v2/oauth/accessToken
  LINE_USER_INFO_ENDPOINT=https://api.line.me/v2/profile
  ```

* `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.line import line_router
  from flask_cors import CORS
  
  
  def create_app():
  
      app = Flask(__name__)
  
      CORS(app, supports_credentials=True)
  
      app.register_blueprint(line_router, url_prefix='/api')
  
      return app
  
  
  app = create_app()
  
  ```

* `be/api/views/line.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 Setting
    line_router = Blueprint('line_router', __name__)
    
    # Client Param
    client_id = os.getenv('LINE_CLIENT_ID')
    client_secret = os.getenv('LINE_CLIENT_SECRET')
    redirect_uri = os.getenv('LINE_REDIRECT_URI')
    
    # LINE Endpoint
    authorization_endpoint = os.getenv('LINE_AUTHORIZATION_ENDPOINT')
    token_endpoint = os.getenv('LINE_TOKEN_ENDPOINT')
    user_info_endpoint = os.getenv('LINE_USER_INFO_ENDPOINT')
    
    app = Flask(__name__)
    
    # Create Authorization Request Endpoint
    @line_router.route("/line/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,
            '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
    @line_router.route("/line/auth_request/complete", methods=['POST'])
    def complete():
        # Parse Req Body
        jsonData = json.dumps(request.json)
        req_body = json.loads(jsonData)
        
        # Token Request
        # https://developers.line.biz/ja/docs/line-login/integrate-line-login-v2/#get-access-token
        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 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']
    
        # Get User Profile
        # https://developers.line.biz/ja/docs/line-login/managing-users/#get-profile
        headers = {
            'Authorization': 'Bearer ' + access_token
        }
        user_info_req = req.Request(
            user_info_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
```



## 参考情報

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