Flask製ブログツールをローカルではなく、パブリックなVM(Digitalocean)に立ててブログを書くことにしたので、念の為、二段階認証機能を実装しました。
実は、ブログに組み入れるのは2回目で、ちょうど2年前の11月に一度実装しています(その後面倒くさくなってやめた)。その時の学んだことは Shizuoka.py#3 で発表させて頂きました(なつかしい)。
当時は二段階認証の名前は聞いたことがあるけど実際にやったことがある人が少なく(Shizuoka.py#3の参加者でも使っている人が10人中1人(私)だった)、発表聴いてくれている方々が理解してくれているかどうか不安でした。それから2年経ち、多くのサービスで利用されるようになってきたので、今現在は実際に使うようになっている方も多いのではないかと思います。
pyotpを使う
pythonでtotpを扱うモジュールはいくつかありますが、私はpyotpを使用しています。シンプルなので気に入っています。 基本動作をIPyhtonで確認してみます
In [1]: import pyotp In [2]: import time In [3]: totp = pyotp.TOTP('JBSWY3DPEHPK3PXP') In [4]: totp.now() Out[4]: '211775' In [5]: totp.verify(211775) Out[5]: True In [6]: time.sleep(30) In [7]: totp.verify(211775) Out[7]: False
因みに上記で記述されているシークレットキー(JBSWY3DPEHPK3PXP)はgoogle authenticatorのサンプルコードでも記述されていて、totpのサンプルで使われるのを時々見かけますが、これはBASE32文字列におけるHexspeak(deadbeef)になっています。その為、サンプルで使われているのかと思慮。
In [1]: import base64 In [2]: import binascii In [3]: binascii.b2a_hex(base64.b32decode('JBSWY3DPEHPK3PXP')) Out[3]: b'48656c6c6f21deadbeef'
Flaskアプリへ組み込む
Flaskアプリへ組み込む場合、べた書きしてもよいのですが、再利用できるように、pyotpを使用して受け取ったワンタイムパスワードが正しいかどうか判定するfunctionを作成します。こんな感じです。
from pyotp import TOTP from datetime import datetime, timedelta def tfa_auth(secret, otp): totp = TOTP(secret) now = datetime.now() past = now + timedelta(seconds=-30) try: return otp == totp.now() or otp == totp.at(past) except: return False
タイムギャップ処理(-30)は必ずつけるのですが、+30をどうするのか悩みます(最近のスマートフォンは必ず時刻同期されているだろうから必要ない?)。
そしてviews側のfunctionはこんなかんじです。この処理の前にユーザー認証処理があって、認証されたらこのfunctonに飛びます。
@app.route('/verify_tfa/', methods=['GET','POST']) def verify_tfa(): error = None if request.method == 'POST': if tfa_auth(app.config['TFA_SECRET'], str(request.form['token'])): session['logged_in'] = True flash(u'You were logged in', 'alert-success') return redirect(url_for('show_entries')) else: error = 'Invalid verification code' return render_template('verify_tfa.html', error = error)
二段階認証機能の実装は、QRコード生成等のステップが無ければ、結構単純なので一度やってみるのもいいと思います。