0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【FastAPI】ゼロトラストを見据えたセキュアな認証APIをゼロから構築する〜ハッシュ化からHttpOnly Cookieまで〜(開発記録Day3)

0
Last updated at Posted at 2026-06-04

はじめに

こんにちは!サイバーセキュリティやゼロトラストアーキテクチャを学んでいる情報系の大学生です。
現在、7日間で「セキュア社内掲示板」をゼロから開発するプロジェクトに挑戦しています。

今日(Day3)は、FastAPIを用いたパスワードの安全なハッシュ化から、ステートレスな通行証であるJWT(JSON Web Token)の発行、そしてXSS攻撃を防ぐHttpOnly Cookieへの格納まで、一連のセキュアな認証フローのバックエンド実装を完了させました!

この記事では、私が実際に直面したバグ(ライブラリのバージョン競合など)の解決策も交えながら、ゼロトラストを見据えたセキュアな認証APIをゼロから構築する手順と、その裏側にあるセキュリティ上の意図(Why)をわかりやすく解説します。

前提条件

  • FastAPI、SQLAlchemyを用いたCRUD処理の基本を理解していること
  • データベースはPostgreSQLを使用

1. パスワードは「平文」で保存しない(ハッシュ化の徹底)

セキュリティの鉄則として、ユーザーのパスワードをそのまま(平文で)データベースに保存してはいけません。万が一データベースが不正アクセスを受けた際、被害を最小限に食い止めるためです。

今回は、強力なハッシュアルゴリズムである bcrypt を使用します。bcrypt は自動でソルト(ランダムな文字列)を付与し、計算をわざと遅延させるストレッチングを行うため、ブルートフォース攻撃やレインボーテーブル攻撃に対して非常に高い耐性を持ちます。

必要なライブラリのインストール

pip install "passlib[bcrypt]"

注意:bcryptのバージョン競合バグについて
2026年現在、passlib と最新の bcrypt (4.x系) の間で互換性エラーが発生し、「72バイト制限のValueError」でサーバーがクラッシュする既知のバグがあります。これを回避するため、実務では bcrypt==3.2.2 にダウングレードして対応しています。

ハッシュ化ロジックの実装

# auth.py
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def get_password_hash(password: str) -> str:
    """平文パスワードをハッシュ化する"""
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """平文パスワードとハッシュ値を照合する"""
    return pwd_context.verify(plain_password, hashed_password)

2. Pydanticスキーマの分離(入り口と出口を分ける)

FastAPIの強力な機能であるPydanticを使って、APIの入力と出力の「型」を定義します。ここで重要なのは、「入力を受け取るスキーマにはパスワードを含めるが、結果を返すスキーマからはパスワード(ハッシュ値含む)を除外する」ことです。

これにより、APIのレスポンスから意図せず機密情報が漏洩するのを防ぎます(情報漏洩時の被害最小化)。

from pydantic import BaseModel

# 入力用(パスワードあり)
class UserCreate(BaseModel):
    username: str
    password: str

# 出力用(パスワードなし)
class UserResponse(BaseModel):
    id: int
    username: str
    model_config = {"from_attributes": True}

3. エラーメッセージの「ぼかし」(ユーザー列挙攻撃の防止)

ログインAPI(照合処理)を実装する際、認証失敗時のエラーメッセージにも気を配る必要があります。

「ユーザーが存在しません」や「パスワードが違います」と具体的に教えてしまうと、攻撃者に「どのIDが実在するか」を教えることになり、リスト型攻撃の標的にされます。これを防ぐため、エラーメッセージは常に曖昧(Incorrect username or password)にします。

@app.post("/login")
def login(user: UserLogin, db: Session = Depends(get_db)):
    db_user = db.query(models.User).filter(models.User.username == user.username).first()
    
    # ユーザーが存在しない、またはパスワードが一致しない場合
    if not db_user or not verify_password(user.password, db_user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password", # 曖昧なメッセージ!
            headers={"WWW-Authenticate": "Bearer"},
        )
    # ... 後続の処理 ...

4. ステートレスな通行証「JWT」の発行

認証を通過したユーザーには、システムを利用するための通行証として JWT(JSON Web Token) を発行します。

JWTはトークン自体にユーザー情報や有効期限を含み、さらにサーバー側の「秘密鍵」による電子署名が付与されています。これにより、サーバーはデータベースに毎回問い合わせることなく、署名を検証するだけでアクセスを許可でき、システムの拡張性(スケーラビリティ)が劇的に向上します。

JWT生成の実装

pip install PyJWT
import jwt
from datetime import datetime, timedelta, timezone

SECRET_KEY = "your-super-secret-key" # 実際は.envで管理する
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    
    # ペイロードと秘密鍵を使って署名付きトークンを作成
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

5. 究極の防御:HttpOnly Cookieによるセッション管理

発行したJWTをフロントエンド(ブラウザ)のどこに保存するかが、最後のセキュリティの関門です。

保存場所 特徴 セキュリティリスク
LocalStorage 扱いが簡単。JSからアクセス可能。 XSS(クロスサイトスクリプティング)攻撃でトークンを容易に盗まれる。
HttpOnly Cookie ブラウザが自動管理。JSからアクセス不可。 トークン窃取のリスクを極小化できる(現代のベストプラクティス)。

FastAPIでCookieにJWTをセットして返す実装は以下のようになります。

from fastapi import Response

@app.post("/login")
def login(user: UserLogin, response: Response, db: Session = Depends(get_db)):
    # ... (認証照合処理) ...
    
    # JWTの生成
    access_token = create_access_token(data={"sub": db_user.username})
    
    # HttpOnly Cookieにトークンをセット
    response.set_cookie(
        key="access_token",
        value=f"Bearer {access_token}",
        httponly=True,   # JSからの読み取りを禁止(XSS対策)
        secure=False,    # 本番環境(HTTPS)ではTrueにする
        samesite="lax",  # CSRF対策
        max_age=1800     # 30分
    )
    
    return {"message": "Login successful"}

Cookieの確認方法に関するTips

httponly=True を設定すると、ブラウザのセキュリティ機構が働き、JavaScriptベースで動いているSwagger UI上の「Response headers」には Set-Cookie が表示されなくなります。
正常にセットされているか確認するには、ブラウザのデベロッパーツール(F12)を開き、「ネットワーク」タブの生レスポンスヘッダーを直接確認する必要があります。


おわりに

これで「誰がアクセスしているかを確実にする(認証)」強固なバックエンドの土台が完成しました。ここで構築した「ステートレスなトークン検証」と「セキュアなCookie管理」は、ゼロトラストアーキテクチャや動的アクセス制御の概念を理解する上での重要な足場となります。

次回はWebAuthn(パスワードレス認証)の実装に入ります!

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?