アーカイブ
はじめに
普段は Django で開発することが多いのですが、あるプロジェクトでリッチな画面が必要となり、FastAPI + Next.js を採用しました。
認証処理の実装方法が Django とは異なるため、今回は FastAPI で認証機能を試しながら理解を深めていきたいと思います。
※本記事は FastAPI の学習・理解を目的としたものであり、本番利用を前提としたものではない点にご留意ください。
環境構築
確認環境
- uv: 0.8.18
- Python: 3.13
uv を使って環境構築をしていきます。
$ uv init
$ uv venv
$ source .venv/bin/activate
FastAPI のパッケージを追加します。
$ uv add "fastapi[standard]"
サンプルのエンドポイントを用意し、簡易的な動作確認をします。
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
$ uv run uvicorn main:app --reload
http://127.0.0.1:8000/docs にアクセスし、Swagger UI からエンドポイントを叩いてみます。
無事動いていることを確認できました。
実装と動作確認
ステップ1. ユーザーの取得
最初に、ユーザー情報を返すエンドポイントを用意し、ダミーの情報を固定で返すようにします。
# main.py
from typing import Annotated
from fastapi import Depends, FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
async def get_current_user():
return User(
username="johndoe",
)
@app.get("/users/me")
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]):
return current_user
read_users_me の引数で current_user: Annotated[User, Depends(get_current_user)] のように指定し、実行時に自動的にユーザーを取得するようにしています。
まだ認証処理を入れていないため、そのまま実行できます。
ステップ2. 認証機能の追加
次に認証機能を追加します。
FastAPI では、OAuth2PasswordBearer を使うことで認証機能を実装できるようです。
# main.py
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
async def get_current_user(token: str = Depends(oauth2_scheme)):
return User(
username="johndoe",
)
@app.get("/users/me")
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]):
return current_user
get_current_user に token: str = Depends(oauth2_scheme) を追加し、ユーザーの取得にアクセストークンが必須となるようにしました。
この状態で Swagger UI を開くと、「Authorize」のボタンが追加され、GET /users/me は認証が必要な状態になっている(鍵マークが付いている)ことがわかります。
実際に実行してみると、 401(Unauthorized) が返ります。
ステップ3. アクセストークンの発行と利用
このままアクセストークンの発行処理と、アクセストークンからユーザーを取得する処理を仮実装していきます。
# main.py
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
class UserInDB(User):
hashed_password: str
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
}
def verify_password(plain_password: str, hashed_password: str) -> bool:
# 本来は bcrypt や Passlib を使ってハッシュと照合する
return "fakehashed" + plain_password == hashed_password
def get_user(db, username: str) -> UserInDB | None:
if username in db:
return UserInDB(**db[username])
return None
def authenticate_user(username: str, password: str) -> User | None:
user = get_user(fake_users_db, username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def decode_token(token):
# 本来は JWT の検証やデコードを行う
user = get_user(fake_users_db, token)
if not user:
return None
return user
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
)
return User(**user.model_dump(exclude={"hashed_password"}))
@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# 便宜上、ユーザー名をアクセストークンとして返す
return {"access_token": user.username, "token_type": "bearer"}
@app.get("/users/me")
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]):
return current_user
いくつか変更箇所があるため、概要を記載します。
・ダミーのDBとして、fake_users_db を用意しました。
・ログイン用のエンドポイント(POST /token)を追加しました。
・フォームデータは OAuth2PasswordRequestForm を使用して受け取っています。
・認証処理自体は authenticate_user の中で行っています。
・ユーザー名とパスワードからユーザーの存在チェックを行い、ユーザーが存在する場合はアクセストークンを生成して返します。
・便宜上、ユーザー名をアクセストークンとして返しています。
・verify_password 関数では、本来 bcrypt や Passlib を使ってパスワードのハッシュと照合しますが、今回は簡易的に 'fakehashed' を用いています。
・get_current_user で、アクセストークンからユーザーを特定して返すようにしました。
・アクセストークンからユーザーを特定する処理は decode_token の中で行っています。
・実際はアクセストークンの偽造や改ざんを防ぐための工夫が必要になります。
実際に動作を確認してみましょう。
「Authorize」からユーザー名とパスワード(今回の例では johndoe/secret)を入力してログインし、アクセストークンを取得します。
認証が通りました。この状態で GET /users/me を実行してみます。
リクエストヘッダーに "Authorization: Bearer {access_token}" が追加され、レスポンスが 200 で返っていることが分かります。
ここまでで基本的な認証フローは実装できました。しかし現状では、アクセストークンの偽造や改ざんは防げません(例えばヘッダーを書き換えてリクエストを送れば他のユーザーへのなりすましができてしまう)。
アクセストークンの偽造や改ざんを防ぐ手段として、JWT 形式でのアクセストークンの発行があります。
JWT形式のアクセストークンは署名つきで発行されるため、トークンの偽造や改ざんを検知することができます。
まとめ
FastAPI の標準機能だけで、簡単にベアラートークン方式の認証を入れられることが分かりました。
実装のポイントは以下の2つです。
・OAuth2PasswordBearer を使い、アクセストークンが必要なエンドポイントの Depends に指定する
・OAuth2PasswordRequestForm を使い、ログインおよびアクセストークン発行用のエンドポイントを実装する
次回は JWT 形式でアクセストークンを発行し、トークンの偽造や改ざんを防げる形にアップデートしていきます。
参考資料
積極採用中!尖ったPythonエンジニアへの第一歩はこちらから