Auth0を用いたセキュアなFastAPIサーバの実装について
今回は、Auth0を用いてFastAPIサーバをセキュアに構築する方法について共有します。
Auth0とは?
Auth0は認証認可を簡単に実装するために利用できるプラットフォームであり、ECサイトからアプリケーション開発者まで幅広く利用されています。様々なアプリケーションに簡単に組み込むことができる上に、実現できるセキュアな状態のレベルが高いことから、多くの場面で利用されていることを目にします。
今回やりたいこと
今回はFastAPIサーバを立てる時に、Auth0を用いてアクセス制限等の認証認可を実現したいと思い調べていたところ、以下のチュートリアルが用意されていることを発見し、やってみようと思いました。FastAPIを利用する上でセキュアな構成を取る方法はいくつもあると思いますが、今回はAuth0と組み合わせる方法を身につけたいと思っていたのでこちらをやってみます。
それではチュートリアルをやってみましょう!
このチュートリアルを実施するにあたり、Auth0はすでに利用できる環境であることを前提としたいと思います。
Python環境構築
今回はuv
を利用して以下のように環境を構築しました(チュートリアルとは少し違います)。
uv init fastapi_auth0 -p 3.12
cd fastapi_auth0
uv add fastapi 'uvicorn[standard]' pydantic-settings 'pyjwt[crypto]'
uv
についてはこちらでも解説しているのでぜひ参考にしてください。
パブリックアクセスを実装する
まずはAuth0を利用せずパブリックアクセスできるようなAPIを実装したいと思います。
from fastapi import APIRouter
router = APIRouter()
@router.get("/api/public")
def public():
"""No access token required to access this route"""
result = {
"status": "success",
"msg": ("Hello from a public endpoint! You don't need to be "
"authenticated to see this.")
}
return result
from fastapi import FastAPI
from public_router import router as public_router
app = FastAPI()
app.include_router(public_router)
それでは実行してパブリックサーバを呼び出してみましょう。サーバの起動には以下のコマンドを用います。
uv run uvicorn main:app --host localhost --port 8000
その状態で今回はcurl
を用いてアクセスしてみます。実行すると設定したレスポンスが帰ってくることが確認できます。
curl -X 'GET' --url http://127.0.0.1:8000/api/public
# 結果
{"status":"success","msg":"Hello from a public endpoint! You don't need to be authenticated to see this."}
プライベートアクセスを実装する
それでは次にプライベートアクセスを求めるエンドポイントを実装してみます。テストのため、Auth0を利用せずベアラートークンが設定されているかを確認する仕組みを使ってプライベートアクセスをさせてみます。
from fastapi import APIRouter, Depends
from fastapi.security import HTTPBearer # 👈 new imports
router = APIRouter()
token_auth_scheme = HTTPBearer() # 👈 new code
@router.get("/api/private")
def private(token: str = Depends(token_auth_scheme)):
"""A valid access token is required to access this route"""
result = token.credentials
return result
from fastapi import FastAPI
from public_router import router as public_router
from private_router import router as private_router
app = FastAPI()
app.include_router(public_router)
app.include_router(private_router)
それでは先ほどと同じように実行してみます。サーバを立てた状態でプライベートAPIにアクセスしてみると、以下のようにエラーがでます。
curl -X 'GET' --url http://127.0.0.1:8000/api/private
# 結果
{"detail":"Not authenticated"}
これはベアラートークンが設定されていないので認証されていないことを意味しています。試しにヘッダーにベアラートークン(今回は適当な文字列を指定しています)を指定するとアクセスが成功します。
curl -X 'GET' --url http://127.0.0.1:8000/api/private --header 'Authorization: Bearer hogehogefugafuga'
# 結果
"hogehogefugafuga"
Auth0 APIの設定をする
次からAuth0を利用していきます。それにあたり、Auth0 APIの設定をします。APIの作成はApplications
→ APIs
のように選択します。
作成画面が開いたら以下のように入力します。
Auth0周りの環境変数などの設定
次にAuth0に関する環境変数を設定していきます。.env
に以下の内容を設定してください。各情報はAuth0のダッシュボードから取得してください。
AUTH0_DOMAIN = your.domain.auth0.com
AUTH0_API_AUDIENCE = https://your.api.audience
AUTH0_ISSUER = https://your.domain.auth0.com/
AUTH0_ALGORITHMS = RS256
アプリケーション設定の実装
次にAuth0と連携するためのアプリケーション設定を実装します。
まずは環境変数ファイルを読み込み設定を取得するためのコードを実装します。
from functools import lru_cache
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
auth0_domain: str
auth0_api_audience: str
auth0_issuer: str
auth0_algorithms: str
class Config:
env_file = ".env"
@lru_cache()
def get_settings():
return Settings()
pydantic_settings.BaseSettings
を利用すると.env
ファイルから情報を読み込むことができるようになります。
次に、トークンをチェックする仕組みを実装します。
from typing import Optional # 👈 new imports
import jwt # 👈 new imports
from fastapi import Depends, HTTPException, status # 👈 new imports
from fastapi.security import SecurityScopes, HTTPAuthorizationCredentials, HTTPBearer # 👈 new imports
from application.config import get_settings # 👈 new imports
class UnauthorizedException(HTTPException):
def __init__(self, detail: str, **kwargs):
super().__init__(status.HTTP_403_FORBIDDEN, detail=detail)
class UnauthenticatedException(HTTPException):
def __init__(self):
super().__init__(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Requires authentication"
)
class VerifyToken:
"""Does all the token verification using PyJWT"""
def __init__(self):
self.config = get_settings()
# This gets the JWKS from a given URL and does processing so you can
# use any of the keys available
jwks_url = f'https://{self.config.auth0_domain}/.well-known/jwks.json'
self.jwks_client = jwt.PyJWKClient(jwks_url)
# 👇 new code
async def verify(self,
security_scopes: SecurityScopes,
token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer())
):
if token is None:
raise UnauthenticatedException
# This gets the 'kid' from the passed token
try:
signing_key = self.jwks_client.get_signing_key_from_jwt(
token.credentials
).key
except jwt.exceptions.PyJWKClientError as error:
raise UnauthorizedException(str(error))
except jwt.exceptions.DecodeError as error:
raise UnauthorizedException(str(error))
try:
payload = jwt.decode(
token.credentials,
signing_key,
algorithms=self.config.auth0_algorithms,
audience=self.config.auth0_api_audience,
issuer=self.config.auth0_issuer,
)
except Exception as error:
raise UnauthorizedException(str(error))
return payload
# 👆 new code
UnauthorizedException
とUnauthenticatedException
の二つのカスタムExceptionを実装して、認証認可が失敗した時に発生させるエラーを実装しています。また、VerifyToken
クラスにて指定されたベアラートークンが有効なものであるかをチェックしています。トークンのチェックをするためにPyJWT
やfastapi.security
の各機能を用いてチェックを行っています。
そして、ここで実装したアプリケーションを利用して、プライベートアクセスAPIの設定を以下のように変更します。
from fastapi import APIRouter, Security
from fastapi.security import HTTPBearer # 👈 new imports
from application.utils import VerifyToken # 👈 Import the new class
router = APIRouter()
auth = VerifyToken() # 👈 Get a new instance
@router.get("/api/private")
def private(auth_result: str = Security(auth.verify)):
"""A valid access token is required to access this route"""
return auth_result
先ほどまでは設定されるベアラートークンは形式さえ合っていれば通っていましたが、今回はトークン情報を正式にチェックする仕組みが入っています。
実行してみる
それではAUth0の認証認可を連携させてみましょう。
まずはAuth0にアクセスしてアクセストークンを取得する必要があります。以下のコマンドを用いてアクセス情報を取得します。実行した結果、access_token
に設定されているトークン情報を後ほど利用しますので控えておいてください。
curl -X 'POST' \
--url 'https://<YOUR DOMAIN HERE>/oauth/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data 'client_id=<YOUR CLIENT ID HERE>' \
--data client_secret=<YOUR CLIENT SECRET HERE> \
--data audience=<YOUR AUDIENCE HERE>
# 結果
{"access_token":"...","expires_in":86400,"token_type":"Bearer"}
アクセストークンが取得できたら、以下のようにして先ほどは適当な文字列を指定していたアクセストークンを置き換えます。実行すると認証情報をデコードした内容が取得できます。
curl -X 'GET' --url http://127.0.0.1:8000/api/private --header 'Authorization: Bearer <ACCESS TOKEN>'
# 結果
{"iss":"https://....auth0.com/","sub":"...","aud":"...","iat":1753525039,"exp":1753611439,"gty":"client-credentials","azp":"..."}
ちなみに、JWTトークンの認証を実装した現時点で最初のように無意味なトークンを指定するともちろんエラーが出ます。
curl -X 'GET' --url http://127.0.0.1:8000/api/private --header 'Authorization: Bearer hogehogefugafuga'
# 結果
{"detail":"Not enough segments"}
まとめ
今回はAuth0の認証認可を利用してFastAPIのAPIをセキュアに実装する方法を学びました。Auth0は幅広く利用されている仕組みなので、誰かに公開して利用してもらうようなサービスを検討されている方はぜひAuth0を組み合わせて安全なAPI実装を試してみてはいかがでしょうか。
Discussion