【Python】Flask でBasic/Digest認証を使う

# はじめに

先日、おじさんはREST API をFlask だけで実装しました。勢いに乗って、せっかくなので認証機能を付けてみようと思いました。ド素人ですが、調べているとベーシック認証やDigest認証というものに出会いました。しくみはさておき、コードを動かせたのでメモしておきます。

下記を参考にさせていただきました。(というか、ほぼそのままです。)

- https://keymaso.com/programemory/python/flask-httpauth/

また、本来なら下記のサイトのチュートリアルなどをしっかり読むのが良いと思います。

- [flask_httpauth](https://pypi.org/project/Flask-HTTPAuth/)のチュートリアル：<BR>
  https://flask-httpauth.readthedocs.io/en/latest/
- [requests](https://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/#) のページ：<BR>
https://requests-docs-ja.readthedocs.io/en/latest/user/authentication/

# Basic認証

ユーザ名とパスワードをHTTPリクエストにそのまま書いて送る方式のようです。受け取るサーバ側では、flask_httpauth というモジュールを使用しました。

```shell
python -m pip install --upgrade pip
python -m pip install flask
python -m pip install flask_httpauth 
```
### サーバの用意

動作確認に使用したサーバ側のコードです。パスワードの確認とかもっと便利な実装方法が無いのかなという気もしますが、とりあえず動いたのでメモします。

```Python:server_basicauth.py
from flask import Flask
from flask_httpauth import HTTPBasicAuth

app = Flask(__name__)
auth = HTTPBasicAuth()
id_list = {
    "user001": "password1111",
    "user002": "password1234"
}

@auth.get_password
def get_pw(id):
    id_list.get(id)

@app.route('/')
@auth.login_required
def index(): 
    return "Hello, %s!" % auth.username()

if __name__ == '__main__':
    app.run()
```

とりあえず起動させます。

```
$ python server_basicauth.py
 * Serving Flask app "server_basicauth" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
```

### HTTP リクエストを送る

このサーバにHTTP request を送るところもpython で実装できます。使えるモジュールはいろいろあるようなのですが、ここではrequests を使います。

```python:request_basicauth.py
import requests
for pswd in ["password1111", "password2222"]:
    usr = "user001"
    print("authtication test: user={}, password={}".format(usr, pswd))
    r = requests.get('http://localhost:5000/', auth=(usr, pswd))
    print("response:", r.status_code)
    print(r.content)
    print(r.headers)
    print("------------------------------------------------------")
```

さきのサーバを立ち上げた状態で、別のターミナルで ```python request_basicauth.py```で実行してみると、以下の結果を得ます。

```
authtication test: user=user001, password=password1111
response: 200
b'Hello, user001!'
{'Content-Type': 'text/html; charset=utf-8', 'Content-Length': '15', 'Server': 'Werkzeug/1.0.1 Python/2.7.17', 'Date': 'Sat, 02 Jan 2021 13:06:21 GMT'}
------------------------------------------------------
authtication test: user=user001, password=password2222
response: 401
b'Unauthorized Access'
{'Content-Type': 'text/html; charset=utf-8', 'Content-Length': '19', 'WWW-Authenticate': 'Basic realm="Authentication Required"', 'Server': 'Werkzeug/1.0.1 Python/2.7.17', 'Date': 'Sat, 02 Jan 2021 13:06:22 GMT'}
------------------------------------------------------
```

想定通り、最初のパスワードでは認証が通り200が返りますが、次は401が返ってきます。

また、Chromeなどのブラウザに ```https://localhost:5000```といれるとパスワードを聞かれます。入力すると、Hello user001 が出力されました。

# Digest 認証

こちらも同様に動きました。先ほどのBasic authentication と異なるのは、flask_httpauth で HTTPDigestAuth を使うところくらいです。

まずは、サーバ側。秘密鍵はサーバ側を起動するときに必要になります。ここでは、コードにそのまま埋め込んでいます。

```python:server_httpauth.py
from flask import Flask
from flask_httpauth import HTTPDigestAuth

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret key here'
auth = HTTPDigestAuth()

id_list = {
    "user001": "password1111",
    "user002": "password2222"
}

@auth.get_password
def get_pw(id):
    return id_list.get(id)

@app.route('/')
@auth.login_required
def index(): 
    return "Hello, %s!" % auth.username()

if __name__ == '__main__':
    app.run()
```

クライアント側も同じように requests のみで実装できます。

```Python:request_digestauth.py
import requests
from requests.auth import HTTPDigestAuth

for passwd in ["password1111", "password2222"]:
    r = requests.get('http://localhost:5000/', auth=HTTPDigestAuth('user001', passwd))
    print(r.status_code)
    print(r.headers)
    print(r.content)
```

サーバを起動し、別ターミナルでリクエスト側を動かすと、以下のようになります。

```
200
{'Date': 'Sat, 02 Jan 2021 13:22:43 GMT', 'Vary': 'Cookie', 'Content-Length': '15', 'Content-Type': 'text/html; charset=utf-8', 'Server': 'Werkzeug/1.0.1 Python/2.7.17'}
Hello, user001!
401
{'Content-Length': '19', 'Set-Cookie': 'session=.eJyrVkosLcmIz8vPS05VsqpWUkhSslLyC4nK9HWJyvI18qzyzfXLjAxJLo_K9TWNzIrKjgoJy_WtijTyd3c19A93tVWq1YEYkV-QWFiKMCPK3bcqKjwo1x9ojp-Ra3lUVmRVlItTdpRLpIG_S0pGlLtfDsTsdKAZtQAJAC0F.X_Bzow.s0xYY1rqLai75_11dFa7PMOkhck; HttpOnly; Path=/', 'Vary': 'Cookie', 'Server': 'Werkzeug/1.0.1 Python/2.7.17', 'Date': 'Sat, 02 Jan 2021 13:22:43 GMT', 'Content-Type': 'text/html; charset=utf-8', 'WWW-Authenticate': 'Digest realm="Authentication Required",nonce="56b06c3b32cba70fc9b6de5f3668a59a",opaque="dc3edf86c7a0f63d0dd6487adce2cba8"'}
Unauthorized Access
```

正しいユーザ名とパスワードだとＯＫになり、そうでないと401 Unauthorized Access が返されることが確認出来ました。

# まとめ

とりあえず、Basic認証、Digest 認証を動かせました。

- flask_httpauth のページを読むと情報が多い！token 認証とか、role の設定、session 、サーバ認証などいろいろ書いてありました。使えるかな。。。
- 秘密鍵をgithub などに上げずにCI/CDするための準備が必要かも。
- ユーザ名とパスワードを今回はコードに埋め込んであるけれど、ユーザを逐次増やしていくときは。。。一般的な方法を知りたい。
- flask 全般についてだけれど、コードを整理するときどうしたら良いのかな。

など、徒然に思わるることはそこはかとなくありますが、本日（正月２日目）はこの辺りにしておきます。