【LINEログイン】Flaskでstateの検証する

皆様こんにちは。

今回はpythonのライブラリFlaskを用いて、LINEログインv2.1に対応した、
テストアプリを作りたいと思います。

https://developers.line.biz/ja/docs/line-login/integrate-line-login/

# 1.準備

必要なライブラリをインストールします。
今回は以下のライブラリを利用します。

``` 
Flask
requests
Flask-Session==0.3.0
uwsgi
gunicorn
argparse
```

https://github.com/myucy/line-login-v2.1-tester/blob/master/requirements.txt

# 2.stateの発行
Pythonのライブラリ https://pythonhosted.org/Flask-Session/ を利用します。
今回はテストアプリなので、単純なロジックでランダムな32文字を作成し、
これをsessionとしています。

```python

import random
import string

# セッションのためのユニークキーの作成

def randomstate(n):
    randlst = [random.choice(string.ascii_letters + string.digits)
               for i in range(n)]
    return ''.join(randlst)

# メインページ。sessionライブラリでstate管理
@app.route('/', methods=['GET'])
def Mainpage():
    # randomstate(n)でstate長を制御
    session["state"] = randomstate(32)
    return render_template('login.html',
                           state=session["state"]
                           )

```


# 3.LINEログインの認可リクエストの作成
POSTした内容を元にLINEログインの認可リクエストを作成します。
https://developers.line.biz/ja/docs/line-login/integrate-line-login/#making-an-authorization-request
scopeが複数してされた場合であっても、対応できるようにscopeの値をループして取り出しています。

```python
from argparse import ArgumentParser
import json
import urllib.request
import requests

from flask import Flask, request, abort, render_template, jsonify, redirect, session
# LINEログインの認可リクエストの生成
@app.route('/login', methods=['POST'])
def authorizeReq():
    scopes = ""
    i = 0
    for key in request.form.getlist("ScopeValue"):
        if (i < 1):
            i = i + 1
            scopes = key
        else:
            scopes = scopes+" "+key
    queries = {}
    queries['response_type'] = 'code'
    queries['client_id'] = request.form['ChannelIdValue']
    queries['redirect_uri'] = request.form['redirect_uriValue']
    queries['scope'] = scopes
    queries['state'] = request.form['stateValue']
    queries['prompt'] = request.form['promptValue']
    queries['bot_prompt'] = request.form['bot_promptValue']
    queries['nonce'] = request.form['nonceValue']
    queries['max_age'] = request.form['max_ageValue']
    queries['ui_locales'] = request.form['ui_localesValue']
    authorize_url = 'https://access.line.me/oauth2/v2.1/authorize?' + \
        urllib.parse.urlencode(queries)
    return redirect(authorize_url)


```

# 4.stateの検証
単純にstateの検証および、エラー時のハンドリング(認可リクエストをキャンセルした場合)を行います。

```python
from argparse import ArgumentParser
import json
import urllib.request
import requests

from flask import Flask, request, abort, render_template, jsonify, redirect, session

@app.route('/callback', methods=['GET'])
def Callbackpage():
    state = request.args.get('state')
    error = request.args.get('error')
    code = request.args.get('code')
    # ローカルで試す場合は uri = request.base_url
    # 外部サーバで試す場合はuriにHTTTPSから始まるアドレスを指定
    uri = "コールバックURLを入力"

    error_description = request.args.get('error_description')

    # エラーハンドリングよりも先にstateの検証を行う
    expected_state = session.get('state')
    if state != expected_state:
        return "[Error] state does not match", 400


    # 認可リクエストをキャンセルした場合などのerrorの制御
    if error:
        return "[Error] Not Logined: " + error + "\n" + error_description, 400

    return render_template('callback.html',
                           code=code,
                           state=state,
                           uri=uri
                           )
```

# 5.上記内容を元に実装したテストアプリ
テストアプリではアクセストークンおよびIDトークンのデコードも実装しています。
https://myucy-login.herokuapp.com/

# 6.メモ

Flask-Sessionライブラリを利用することで非常に簡単にCSRF対策ができるので
ぜひ実装してください。
そういえば、認証バッジ付きの公式アカウントのLINEログインでもstateが固定なアカウントを
ちらほら見かけますが、あれは大丈夫なんでしょうか？

# 7.おまけ
ソースコード
https://github.com/myucy/line-login-v2.1-tester