19
18

fastapi-usersでJWT認証 + MFAを使ったログイン実装

Posted at

FastAPIを利用したことはあるものの、fastapi-usersを使ったことがなかったため、トークン認証と多要素認証(MFA)の実装してみました。fastapi-usersを利用すると、ユーザー認証や管理が簡単に実装できるようなので、どのように動作するのかも含め検証してみました。

前提(Docker環境)

Python: 3.9.2
pip3: 24.2
fastapi: 0.112.0
fastapi-users: 13.0.0
SQLAlchemy: 2.0.32

1. React/FastAPIコード

今回はGithubにアップ済みのコードを使って進めたいと思います。

sample code

2. ディレクトリ構成

GitHubに登録されているSourceコードは以下の通りです。

.
└── SecureAuthSite/
    ├── api/
    │   ├── main.py              # FastAPI/Endpoint/ルータ設定
    │   ├── auth.py              # 認証関連
    │   ├── schemas.py           # クラス定義
    │   └── database.py          # DB関連
    └── react/
        ├── .env                 # PORT/API向けURL設定
        ├── package.json
        ├── package-lock.json
        └── src/
            ├── App.js           # ルーティング
            ├── LoginForm.js     # ログイン画面
            ├── RegisterForm.js  # ユーザ登録用
            ├── TfaForm.js       # MFAチェック画面
            ├── Dashboard.js     # ログイン後画面
            └── TotpSetup.js     # MFA登録


こちらのコードをコンテナ上でcloneしていただき動作確認をしていきたいと思います。

3. main.py / シーケンス

Sourceコードも複数ありますので、main.pyのみ記載いたします。

main.py
from fastapi_users import schemas
from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from api.auth import fastapi_users, auth_backend, current_active_user, get_user_manager, get_jwt_strategy
from api.schemas import TOTPRequest, TfaRequest
from api.database import engine, Base
import uuid
import pyotp

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.on_event("startup")
async def on_startup():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

@app.get("/auth/check-totp")
async def check_totp(user=Depends(current_active_user)):
    if not user.totp_secret:
        return {"totp_setup_required": True}
    return {"totp_required": True}

@app.get("/auth/totp-setup")
async def totp_setup(user=Depends(current_active_user), user_manager=Depends(get_user_manager)):
    if not user.totp_secret:
        user.totp_secret = pyotp.random_base32()
        update_dict = {"totp_secret": user.totp_secret}
        await user_manager.user_db.update(user, update_dict)

    totp_uri = pyotp.TOTP(user.totp_secret).provisioning_uri(user.email, issuer_name="DemoSite")
    return {"totp_uri": totp_uri}

@app.post("/auth/setup-totp")
async def setup_totp(request: TOTPRequest, user=Depends(current_active_user), user_manager=Depends(get_user_manager)):
    totp = pyotp.TOTP(user.totp_secret)
    if totp.verify(request.otp):
        return {"success": True}
    else:
        return {"success": False, "detail": "Invalid TOTP code"}

@app.post("/auth/login-tfa")
async def login_tfa(request: TfaRequest, user_manager=Depends(get_user_manager)):
    user = await user_manager.get_by_email(request.username)
    if not user or not await user_manager.verify_password(request.password, user.hashed_password):
        raise HTTPException(status_code=400, detail="Invalid username or password")

    totp = pyotp.TOTP(user.totp_secret)
    if not totp.verify(request.otp):
        raise HTTPException(status_code=400, detail="Invalid OTP")

    jwt_strategy = get_jwt_strategy()
    token = await jwt_strategy.write_token(user)
    return {"token": token}

app.include_router(fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"])
app.include_router(fastapi_users.get_register_router(schemas.BaseUser[uuid.UUID], schemas.BaseUserCreate), prefix="/auth", tags=["auth"])
app.include_router(fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"])
app.include_router(fastapi_users.get_verify_router(schemas.BaseUser[uuid.UUID]), prefix="/auth", tags=["auth"])
app.include_router(fastapi_users.get_users_router(schemas.BaseUser[uuid.UUID], schemas.BaseUserUpdate), prefix="/users", tags=["users"])

ポイント(FastAPI-Users/MAF関連)

  • on_startup: アプリケーションの起動時に、データベースのテーブルが自動的に作成される
  • 二要素関連
    • /auth/check-totp: ユーザーにTOTPが設定されているか確認
    • /auth/totp-setup: TOTPシークレットを生成し、それを使ってQRコードを表示するためのURIを返却
    • /auth/setup-totp: ユーザーの入力したTOTPコードが正しいか検証、正しければ成功メッセージを返却
    • /auth/login-tfa: ユーザー名とパスワードを検証した後、TOTPコードをチェックしコードが正しければ、JWTトークンを発行
  • FastAPI-Users関連
    • /auth/jwt: JWTを使った認証ルーターを追加
    • /auth: ユーザー登録、パスワードリセット、メール認証などのルーターを追加
    • /users: ユーザー管理のためのルーターを追加

FastAPI-Usersについては下記マニュアルを参照ください。

またざっくりした流れですが、sequence図にすると以下のような形になります。

4. デプロイ

gitからcloneして、ディレクトリを遷移します。

git clone https://github.com/nw-engineer/SecureAuthSite.git
cd SecureAuthSite

uvicornでAPIを使える状態にしておきます。

uvicorn api.main:app --host 0.0.0.0 --port 4002 --reload

続いて、別ターミナルを起動させてreactの準備をします。ディレクトリ移動後にpackageをインストールします。

cd SecureAuthSite/react
npm install

起動前に.envのIP部分を変更します。

.env
PORT=4001
REACT_APP_API_URL=http://x.x.x.x:4002  # x.x.x.xを自身の環境に合わせる。

Reactを起動しましょう。

npm start

5. 動作確認

それでは、ブラウザからアクセスします。まだユーザが一人も登録されていない状態なので、ユーザ登録から実施します。 http://ipaddoress:4001/register を開きます。mailアドレスとパスワードを入れて、Registerをクリックします。

image.png

続いて、 http://ipaddoress:4001/ にアクセスし、先ほど登録したmail、パスワードを入れてログインをクリックします。
image.png

二要素認証の画面が表示されますので、アプリを登録しTOTPコードを入れて「MFA登録」ボタンをクリックします。
image.png

ダッシュボード画面が表示されました。
image.png

MFA登録後は、QRコード画面の代わりに以下ページが表示され成功するとダッシュボード画面が表示されます。

image.png

デベロッパーツールから、ローカルストレージに保存されているtokenを削除して/dashboardにアクセスするとログイン画面が強制されていることがわかります。

6. その他

ユーザー認証、登録、パスワードリセット、メール確認、OAuth2(Google, Facebookなど)によるログインなど、標準的なユーザー管理機能をすぐに利用できるのは便利ですね。

19
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
18