はじめに
Pythonを使ったwebアプリにQRcodeを使用したTOTP「Time-based One-Time Password(時間ベースのワンタイムパスワード)」認証を導入する手順を簡潔にまとめてみました。
まだ実装したことがない方の参考になれば嬉しいです。
(ユーザー登録・パスワード認証は実装されている前提で進めさせていただきます)
1. 必要なライブラリをインストール
pip install pyotp
(ワンタイムパスワードを生成・検証するためのライブラリ)
pip install qrcode
(QRコードを生成するライブラリ)
「requirements.txt」を使用する場合は、
pyotp
qrcode
COPY requirements.txt .
RUN pip install -r requirements.txt
2. カスタムユーザーモデルにtotp用フィールドの追加
Django には標準で User モデルが用意されています(django.contrib.auth.models.User)が、
認証周りを細かくカスタマイズしたい場合、AbstractBaseUser を継承して自分で定義する必要があります。
from django.contrib.auth.models import AbstractBaseUser
class User(AbstractBaseUser):
# TOTP用のシークレットキーを保存するフィールドを追加
totp_secret = models.CharField(max_length=32, blank=True, null=True)
# TOTP用のシークレットキーを生成する関数
def generate_totp_secret(self):
self.totp_secret = pyotp.random_base32()
self.save()
# ユーザーのTOTPコードを検証する関数
def verify_totp(self, totp_code):
if not self.totp_secret:
return False
totp = pyotp.TOTP(self.totp_secret)
return totp.verify(totp_code)
3. QRcodeを表示する
各ユーザーがパスワード認証を突破したタイミングで、そのユーザーが持つシークレットキーから生成されたQRcodeを表示します。
from django.contrib.auth import get_user_model
User = get_user_model()
def generate_qr(request, username):
user = User.objects.get(username=username)
# まだTOTPが設定されていない場合、新しいシークレットを生成
if not user.totp_secret:
user.generate_totp_secret()
totp = pyotp.TOTP(user.totp_secret)
totp_uri = totp.provisioning_uri(name=user.username, issuer_name="YourAppName")
# QRコードを生成
qr = qrcode.make(totp_uri)
buffer = BytesIO()
qr.save(buffer, format="PNG")
buffer.seek(0)
return HttpResponse(buffer.getvalue(), content_type="image/png")
from django.urls import path
from . import views
app_name = "accounts"
urlpatterns = [
path('generate_qr/<str:username>/', views.generate_qr, name='generate_qr')
]
<img id="qrCodeImage" src="" alt="Scan this QR code with Your MFA App">
let qrImage = document.getElementById("qrCodeImage");
let qrUrl = `/accounts/generate_qr/${username}/`;
qrImage.src = qrUrl;
4. 入力codeのバリデーション
入力フォームなどから受け取ったcodeを検証し、認証が成功すればログイン処理に進めます。
def totpLogin(request):
username = request.POST.get("username")
user = User.objects.get(username=username)
totp_code = request.POST.get("totp_code")
totp_code = int(totp_code)
if not user.verify_totp(totp_code):
return HttpResponseForbidden("Invalid TOTP code") # 認証失敗
login(request, user)
おわりに
今回はTOTPの実装部分だけをぎゅっとまとめて紹介しました。
意外と少ないコードで実装できるんだなと驚きました。
実際に認証機能として実装する際は、
・フォーマットのバリデーション/エラーハンドリングの追加
・ユーザーが設定画面からTOTP機能をON/OFFできるようにする
・上記のタイミングでtotp_secretをリフレッシュする
・MFA App(認証端末)側でtotp_secretを削除してしまった場合のバックアップ方法
などが必要になるかと思います。
認証まわりに関連して、JWT(JSON Web Token)を導入する記事も書きましたので、ぜひそちらも合わせて御覧ください!
PythonでJWT認証を実装する