pyotpでワンタイムパスワードを試してみる

2024.06.24

はじめに

データアナリティクス事業本部のkobayashiです。

Pythonで2FAやMFA用のワンタイムパスワード(OTP: One-Time Password)を生成および検証するためのライブラリを試してみたのでその内容をまとめます。

pyotpとは

pyotpはOTPの生成と検証を行うことができるライブラリで、TOTP(Time-base One-Time Password)とHOTP(HMAC-based One-Time Password)の両方を扱うことができます。実装は非常に簡単に行えますが公式ドキュメントにもある通り認証の核になるものなので組み込む際にはチェックリストの内容に従う必要があります。

Implementers should read and follow the HOTP security requirements and TOTP security considerations sections of the relevant RFCs. At minimum, application implementers should follow this checklist:

pyauth/pyotp: Python One-Time Password Library

pyotpではTOTPとHOTPの2種類がありますが次のようにに使い分けます。

  • TOTP(Time-base One-Time Password)
    • 指定された時間でOTPを更新する。Google Authenticatorや1PasswordなどのOTPクライアントアプリで生成されたOTPを認証に使う。
  • HOTP(HMAC-based One-Time Password)
    • カウンターでOTPを更新する。e-mailやsmsでOTPが送られてきたOTPを入力して認証を行う。

pyotpを試してみる

今回はGoogle Authenticatorを使うTOTPを検証してみます。

環境

  • Python 3.11.8
  • pyotp 2.9.0
  • qrcode 7.4.2

実装してみる

はじめにpyotpをインストールしますがこれはいつも通りpipでインストールできます。またqrコードを作成するバッケージも一緒にインストールします。

$ pip install pyotp
Collecting pyotp
  Using cached pyotp-2.9.0-py3-none-any.whl (13 kB)
Installing collected packages: pyotp
Successfully installed pyotp-2.9.0

$ pip install qrcode
Installing collected packages: pypng, qrcode
Successfully installed pypng-0.20220715.0 qrcode-7.4.2

今回は以下の流れで実装してみます。

  1. 鍵を作成する(get_secret_key関数)
  2. 作成した秘密鍵を環境変数に保存し、その秘密鍵でTOTPオブジェクトを作成してからOTPクライントアプリで読み込めるQRコードを作成する(get_qr関数)
  3. OTPクライアントアプリで発行したOTPを検証する(confirm_totp関数)

以下がソースコードになります。

sample_otp.py

import pyotp
import qrcode
import os


def get_secret_key():
    # base32のシークレットキーを発行
    print(pyotp.random_base32())


def get_qr():
    # timeベースのotpの生成
    totp = pyotp.TOTP(os.environ.get('OTP_SEC_KEY'))
    # qr生成
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=5,
        border=2,
    )
    qr.add_data(totp.provisioning_uri(name="otp_sample_user", issuer_name="otp_sample_proj"))
    qr.make(fit=True)
    img = qr.make_image(fill_color="black", back_color="white")
    img.save("otp.png")


def confirm_totp(totp_code: str):
    # 検証
    totp = pyotp.TOTP(os.environ.get('OTP_SEC_KEY'))
    print(totp.verify(totp_code))


if __name__ == "__main__":
    while True:
        totp_code = input('totp_code: ')
        confirm_totp(totp_code)

それでは早速試してみます。

まずはじめにOTPで使用する鍵をpyotpパッケージで作成します。

$ python -c "import sample_otp; sample_otp.get_secret_key()"
YL32RNB5RS2PPGLFMCQO2LDCDTCZN5KN

次にOTPクライアントで読み込むためのQRコードを作成します。

環境変数OTP_SEC_KEYに保存しQRの発行とOTPの検証にこの環境変数を使います。本来の使い方としてはこの鍵をユーザーのパスワードと同様に取り扱いDBなどに保存して使うべきですが、今回はあくまでpyotpの検証なので簡易に環境変数に保存して使っています。

$ export OTP_SEC_KEY=YL32RNB5RS2PPGLFMCQO2LDCDTCZN5KN
$ python -c "import sample_otp; sample_otp.get_qr()"

実行するとotp.pngのファイルが作成されるので開くと以下のようなQRコードが表示されます。

今回MFAクライアントとしてGoogle Authenticatorで読み込むと以下のようなOTPが作成されます。

では最後にGoogle Authenticatorで表示されるOTPを検証してみます。

$ python sample_otp.py
totp_code: 140408
True
totp_code: 123456
False
### 30秒以上待つ
totp_code: 140408
False
totp_code: 971698
True

このようにMFAクライアントで生成されたOTPを検証するとTrueを返し、適当な6桁の数字を入れたり有効期限が切れるとFalseとなることがわかります。

まとめ

OTPを生成および検証するためのライブラリpyotpを試してみました。特に難しいことなく公式ドキュメントに記載されている方法で実装することで少ないコードでOTPの仕組みを使うことができました。認証が必要なWebアプリにも簡単に組み込めるかと思います。

最後まで読んで頂いてありがとうございました。