RBAC と JWT 検証で Django REST Framework API を保護する
このガイドでは、Logto が発行する ロールベースのアクセス制御 (RBAC) と JSON Web Token (JWT) を使用して Django REST Framework API に認可 (Authorization) を実装し、セキュリティを強化する方法を説明します。
始める前に
クライアントアプリケーションは Logto から アクセス トークン (Access token) を取得する必要があります。まだクライアント統合を設定していない場合は、React、Vue、Angular などのクライアントフレームワーク向け クイックスタート や、サーバー間アクセス用の マシン間通信 (M2M) ガイド をご覧ください。
このガイドは、Django REST Framework アプリケーションにおけるこれらのトークンの サーバーサイド検証 に焦点を当てています。

学べること
- JWT 検証:アクセス トークン (Access token) を検証し、認証 (Authentication) 情報を抽出する方法
- ミドルウェア実装:API 保護のための再利用可能なミドルウェアの作成
- 権限モデル:さまざまな認可 (Authorization) パターンの理解と実装
- アプリケーション全体のエンドポイント向けグローバル API リソース
- テナント固有の機能制御のための組織 (Organization) 権限
- マルチテナントデータアクセスのための組織レベル API リソース
- RBAC 統合:API エンドポイントでロールベースの権限 (Permission) とスコープ (Scope) を強制する方法
前提条件
- Python の最新安定版がインストールされていること
- Django REST Framework および Web API 開発の基礎知識
- Logto アプリケーションが設定済み(必要に応じて クイックスタート を参照)
権限モデルの概要
保護を実装する前に、アプリケーションアーキテクチャに適した権限モデルを選択してください。これは Logto の 3 つの主要な 認可 (Authorization) シナリオ に対応しています:
- グローバル API リソース
- 組織(非 API)権限
- 組織レベル API リソース

- ユースケース: アプリケーション全体で共有される API リソースを保護する(組織固有ではない)
- トークンタイプ: グローバルオーディエンスを持つアクセス トークン
- 例: パブリック API、コアプロダクトサービス、管理エンドポイント
- 最適: すべての顧客が利用する API を持つ SaaS プロダクト、テナント分離のないマイクロサービス
- 詳細: グローバル API リソースの保護

- ユースケース: 組織固有のアクション、UI 機能、ビジネスロジックを制御する(API ではない)
- トークンタイプ: 組織固有オーディエンスを持つ組織トークン
- 例: 機能ゲーティング、ダッシュボード権限、メンバー招待コントロール
- 最適: 組織固有の機能やワークフローを持つマルチテナント SaaS
- 詳細: 組織(非 API)権限の保護

- ユースケース: 特定の組織コンテキスト内でアクセス可能な API リソースを保護する
- トークンタイプ: API リソースオーディエンス + 組織コンテキストを持つ組織トークン
- 例: マルチテナント API、組織スコープのデータエンドポイント、テナント固有のマイクロサービス
- 最適: API データが組織スコープとなるマルチテナント SaaS
- 詳細: 組織レベル API リソースの保護
💡 進める前にモデルを選択してください — このガイド全体で選択したアプローチを参照します。
クイック準備手順
Logto リソースと権限の設定
- グローバル API リソース
- 組織(非 API)権限
- 組織レベル API リソース
- API リソースの作成: コンソール → API リソース にアクセスし、API を登録します(例:
https://api.yourapp.com
) - 権限の定義:
read:products
やwrite:orders
などのスコープを追加します – 権限付き API リソースの定義 を参照 - グローバルロールの作成: コンソール → ロール にアクセスし、API 権限を含むロールを作成します – グローバルロールの設定 を参照
- ロールの割り当て: API アクセスが必要なユーザーまたは M2M アプリケーションにロールを割り当てます
- 組織権限の定義: 組織テンプレートで
invite:member
やmanage:billing
などの非 API 組織権限を作成します - 組織ロールの設定: 組織テンプレートで組織固有のロールを設定し、それらに権限を割り当てます
- 組織ロールの割り当て: 各組織コンテキスト内でユーザーに組織ロールを割り当てます
- API リソースの作成: 上記と同様に API リソースを登録しますが、組織コンテキストで使用します
- 権限の定義: 組織コンテキストにスコープされた
read:data
やwrite:settings
などのスコープを追加します - 組織テンプレートの設定: API リソース権限を含む組織ロールを設定します
- 組織ロールの割り当て: API 権限を含む組織ロールにユーザーまたは M2M アプリケーションを割り当てます
- マルチテナント設定: API が組織スコープのデータとバリデーションを処理できることを確認します
ロールベースのアクセス制御ガイド からステップバイステップのセットアップ手順を始めましょう。
クライアントアプリケーションの更新
クライアントで適切なスコープをリクエストする:
- ユーザー認証 (Authentication): アプリの更新 → で API スコープや組織コンテキストをリクエスト
- マシン間通信: M2M スコープの設定 → でサーバー間アクセスを設定
通常、クライアント設定を次のいずれか、または複数を含めるように更新します:
- OAuth フローでの
scope
パラメーター - API リソースアクセス用の
resource
パラメーター - 組織コンテキスト用の
organization_id
テストするユーザーまたは M2M アプリが、API に必要な権限を含む適切なロールまたは組織ロールに割り当てられていることを確認してください。
API プロジェクトの初期化
新しい Django REST Framework プロジェクトを初期化するには:
django-admin startproject your_api_name
cd your_api_name
必要なパッケージをインストールします:
pip install Django djangorestframework
基本的な Django アプリを作成します:
python manage.py startapp api
settings に DRF を追加します:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'api',
]
基本的な API ビューを作成します:
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(['GET'])
def hello_view(request):
return Response({"message": "Hello from Django REST Framework"})
URL 設定を追加します:
from django.urls import path
from . import views
urlpatterns = [
path('', views.hello_view, name='hello'),
]
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
]
開発サーバーを起動します:
python manage.py runserver
シリアライザーやビューセット、その他の機能のセットアップ方法については、Django REST Framework のドキュメントを参照してください。
定数とユーティリティの初期化
トークンの抽出と検証を処理するために、コード内で必要な定数やユーティリティを定義してください。有効なリクエストには、Authorization
ヘッダーが Bearer <アクセス トークン (Access token)>
の形式で含まれている必要があります。
JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks'
ISSUER = 'https://your-tenant.logto.app/oidc'
class AuthInfo:
def __init__(self, sub: str, client_id: str = None, organization_id: str = None,
scopes: list = None, audience: list = None):
self.sub = sub
self.client_id = client_id
self.organization_id = organization_id
self.scopes = scopes or []
self.audience = audience or []
def to_dict(self):
return {
'sub': self.sub,
'client_id': self.client_id,
'organization_id': self.organization_id,
'scopes': self.scopes,
'audience': self.audience
}
class AuthorizationError(Exception):
def __init__(self, message: str, status: int = 403):
self.message = message
self.status = status
super().__init__(self.message)
def extract_bearer_token_from_headers(headers: dict) -> str:
"""
HTTP ヘッダーからベアラートークンを抽出します。
注意: FastAPI および Django REST Framework には組み込みのトークン抽出機能があります。
この関数は主に Flask やその他のフレームワーク向けです。
"""
authorization = headers.get('authorization') or headers.get('Authorization')
if not authorization:
raise AuthorizationError('Authorization header is missing', 401)
if not authorization.startswith('Bearer '):
raise AuthorizationError('Authorization header must start with "Bearer "', 401)
return authorization[7:] # 'Bearer ' プレフィックスを削除
Logto テナント情報の取得
Logto が発行したトークンを検証するには、次の値が必要です:
- JSON Web Key Set (JWKS) URI:JWT 署名を検証するために使用される Logto の公開鍵の URL。
- 発行者 (Issuer):期待される発行者値(Logto の OIDC URL)。
まず、Logto テナントのエンドポイントを見つけます。これはさまざまな場所で確認できます:
- Logto コンソールの 設定 → ドメイン で確認できます。
- Logto で設定した任意のアプリケーション設定内の 設定 → エンドポイント & 資格情報 で確認できます。
OpenID Connect ディスカバリーエンドポイントから取得する
これらの値は、Logto の OpenID Connect ディスカバリーエンドポイントから取得できます:
https://<your-logto-endpoint>/oidc/.well-known/openid-configuration
以下はレスポンス例です(他のフィールドは省略しています):
{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}
コード内にハードコーディングする(推奨されません)
Logto では JWKS URI や発行者 (Issuer) のカスタマイズができないため、これらの値をコード内にハードコーディングすることも可能です。ただし、将来的に設定が変更された場合のメンテナンス負荷が増加する可能性があるため、本番アプリケーションでは推奨されません。
- JWKS URI:
https://<your-logto-endpoint>/oidc/jwks
- 発行者 (Issuer):
https://<your-logto-endpoint>/oidc
トークンと権限の検証
トークンを抽出し OIDC 設定を取得した後、次の点を検証してください:
- 署名:JWT は有効であり、Logto(JWKS 経由)によって署名されている必要があります。
- 発行者 (Issuer):Logto テナントの発行者 (Issuer) と一致している必要があります。
- オーディエンス (Audience):Logto に登録された API のリソースインジケーター、または該当する場合は組織コンテキストと一致している必要があります。
- 有効期限:トークンが有効期限切れでないこと。
- 権限 (スコープ) (Permissions (scopes)):トークンに API / アクションに必要なスコープが含まれている必要があります。スコープは
scope
クレーム内のスペース区切り文字列です。 - 組織コンテキスト:組織レベルの API リソースを保護する場合、
organization_id
クレームを検証してください。
JWT の構造やクレームについて詳しくは JSON Web Token を参照してください。
各権限モデルで確認すべきこと
クレームや検証ルールは権限モデルによって異なります:
- グローバル API リソース
- 組織(非 API)権限
- 組織レベル API リソース
- オーディエンスクレーム (
aud
): API リソースインジケーター - 組織クレーム (
organization_id
): なし - チェックするスコープ(権限) (
scope
): API リソース権限
- オーディエンスクレーム (
aud
):urn:logto:organization:<id>
(組織コンテキストがaud
クレームに含まれる) - 組織クレーム (
organization_id
): なし - チェックするスコープ(権限) (
scope
): 組織権限
- オーディエンスクレーム (
aud
): API リソースインジケーター - 組織クレーム (
organization_id
): 組織 ID(リクエストと一致する必要あり) - チェックするスコープ(権限) (
scope
): API リソース権限
非 API 組織権限の場合、組織コンテキストは aud
クレーム(例:urn:logto:organization:abc123
)で表されます。organization_id
クレームは組織レベル API リソーストークンにのみ存在します。
セキュアなマルチテナント API のため、必ず権限(スコープ)とコンテキスト(オーディエンス、組織)の両方を検証してください。
検証ロジックの追加
PyJWT を使用して JWT の検証を行います。まだインストールしていない場合は、次のコマンドでインストールしてください:
pip install pyjwt[crypto]
まず、JWT 検証を処理するための共通ユーティリティを追加します:
import jwt
from jwt import PyJWKClient
from typing import Dict, Any
from auth_middleware import AuthInfo, AuthorizationError, JWKS_URI, ISSUER
jwks_client = PyJWKClient(JWKS_URI)
def validate_jwt(token: str) -> Dict[str, Any]:
"""JWT を検証し、ペイロードを返す"""
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
issuer=ISSUER,
options={'verify_aud': False} # オーディエンスは手動で検証します
)
verify_payload(payload)
return payload
except jwt.InvalidTokenError as e:
raise AuthorizationError(f'Invalid token: {str(e)}', 401)
except Exception as e:
raise AuthorizationError(f'Token validation failed: {str(e)}', 401)
def create_auth_info(payload: Dict[str, Any]) -> AuthInfo:
"""JWT ペイロードから AuthInfo を作成"""
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
audience = payload.get('aud', [])
if isinstance(audience, str):
audience = [audience]
return AuthInfo(
sub=payload.get('sub'),
client_id=payload.get('client_id'),
organization_id=payload.get('organization_id'),
scopes=scopes,
audience=audience
)
def verify_payload(payload: Dict[str, Any]) -> None:
"""権限モデルに基づいてペイロードを検証"""
# 権限モデルに基づく検証ロジックをここに実装してください
# 下記の権限モデルセクションで説明します
pass
次に、アクセストークンを検証するミドルウェアを実装します:
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
from jwt_validator import validate_jwt, create_auth_info
class AccessTokenAuthentication(TokenAuthentication):
keyword = 'Bearer' # 'Token' の代わりに 'Bearer' を使用
def authenticate_credentials(self, key):
"""
トークンを JWT として検証し、認証 (Authentication) します。
"""
try:
payload = validate_jwt(key)
auth_info = create_auth_info(payload)
# 汎用的に認証情報を保持するユーザーライクなオブジェクトを作成
user = type('User', (), {
'auth': auth_info,
'is_authenticated': True,
'is_anonymous': False,
'is_active': True,
})()
return (user, key)
except AuthorizationError as e:
if e.status == 401:
raise exceptions.AuthenticationFailed(str(e))
else: # 403
raise exceptions.PermissionDenied(str(e))
権限モデルに応じて、jwt_validator.py
内で適切な検証ロジックを実装してください:
- グローバル API リソース
- 組織(非 API)権限
- 組織レベル API リソース
def verify_payload(payload: Dict[str, Any]) -> None:
"""グローバル API リソース用のペイロードを検証"""
# オーディエンスクレームが API リソースインジケーターと一致するか確認
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Invalid audience')
# グローバル API リソースに必要なスコープを確認
required_scopes = ['api:read', 'api:write'] # 実際に必要なスコープに置き換えてください
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient scope')
def verify_payload(payload: Dict[str, Any]) -> None:
"""組織権限用のペイロードを検証"""
# オーディエンスクレームが組織フォーマットと一致するか確認
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
has_org_audience = any(aud.startswith('urn:logto:organization:') for aud in audiences)
if not has_org_audience:
raise AuthorizationError('Invalid audience for organization permissions')
# 組織 ID がコンテキストと一致するか確認(リクエストコンテキストから取得する必要があります)
expected_org_id = 'your-organization-id' # リクエストコンテキストから取得
expected_aud = f'urn:logto:organization:{expected_org_id}'
if expected_aud not in audiences:
raise AuthorizationError('Organization ID mismatch')
# 必要な組織スコープを確認
required_scopes = ['invite:users', 'manage:settings'] # 実際に必要なスコープに置き換えてください
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient organization scope')
def verify_payload(payload: Dict[str, Any]) -> None:
"""組織レベル API リソース用のペイロードを検証"""
# オーディエンスクレームが API リソースインジケーターと一致するか確認
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Invalid audience for organization-level API resources')
# 組織 ID がコンテキストと一致するか確認(リクエストコンテキストから取得する必要があります)
expected_org_id = 'your-organization-id' # リクエストコンテキストから取得
org_id = payload.get('organization_id')
if expected_org_id != org_id:
raise AuthorizationError('Organization ID mismatch')
# 組織レベル API リソースに必要なスコープを確認
required_scopes = ['api:read', 'api:write'] # 実際に必要なスコープに置き換えてください
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient organization-level API scopes')
ミドルウェアを API に適用する
これで、保護された API ルートにミドルウェアを適用します。
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.response import Response
from auth_middleware import AccessTokenAuthentication
@api_view(['GET'])
@authentication_classes([AccessTokenAuthentication])
def protected_view(request):
# request.user.auth から認証情報 (auth information) にアクセス
return Response({"auth": request.user.auth.to_dict()})
またはクラスベースビューを使用する場合:
from rest_framework.views import APIView
from rest_framework.response import Response
from auth_middleware import AccessTokenAuthentication
class ProtectedView(APIView):
authentication_classes = [AccessTokenAuthentication]
def get(self, request):
# request.user.auth から認証情報 (auth information) にアクセス
return Response({"auth": request.user.auth.to_dict()})
from django.urls import path
from . import views
urlpatterns = [
path('api/protected/', views.protected_view, name='protected'),
# クラスベースビューの場合は以下を使用:
# path('api/protected/', views.ProtectedView.as_view(), name='protected'),
]
保護された API のテスト
アクセス トークン (Access tokens) の取得
クライアントアプリケーションから: クライアント統合を設定している場合、アプリは自動的にトークンを取得できます。アクセス トークン (Access token) を抽出し、API リクエストで使用してください。
curl / Postman でのテスト用:
-
ユーザートークン: クライアントアプリの開発者ツールを使い、localStorage またはネットワークタブからアクセス トークン (Access token) をコピーします。
-
マシン間通信トークン: クライアントクレデンシャルフローを使用します。以下は curl を使った非公式な例です:
curl -X POST https://your-tenant.logto.app/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=your-m2m-client-id" \
-d "client_secret=your-m2m-client-secret" \
-d "resource=https://your-api-resource-indicator" \
-d "scope=api:read api:write"resource
やscope
パラメーターは API リソースや権限に応じて調整が必要です。API が組織スコープの場合はorganization_id
パラメーターも必要になる場合があります。
トークンの内容を確認したい場合は、 JWT デコーダー を使って JWT をデコード・検証できます。
保護されたエンドポイントのテスト
有効なトークンリクエスト
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected
期待されるレスポンス:
{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
トークンなし
curl http://localhost:3000/api/protected
期待されるレスポンス (401):
{
"error": "Authorization header is missing"
}
無効なトークン
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected
期待されるレスポンス (401):
{
"error": "Invalid token"
}
権限モデルごとのテスト
- グローバル API リソース
- 組織 (非 API) 権限
- 組織レベル API リソース
グローバルスコープで保護された API のテストシナリオ:
- 有効なスコープ: 必要な API スコープ(例:
api:read
,api:write
)を含むトークンでテスト - スコープ不足: 必要なスコープがない場合は 403 Forbidden を期待
- 誤ったオーディエンス: オーディエンスが API リソースと一致しない場合は 403 Forbidden を期待
# 必要なスコープがないトークン - 403 を期待
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected
組織固有のアクセス制御のテストシナリオ:
- 有効な組織トークン: 正しい組織コンテキスト(組織 ID とスコープ)を含むトークンでテスト
- スコープ不足: ユーザーが要求された操作の権限を持たない場合は 403 Forbidden を期待
- 誤った組織: オーディエンスが組織コンテキスト(
urn:logto:organization:<organization_id>
)と一致しない場合は 403 Forbidden を期待
# 別の組織用トークン - 403 を期待
curl -H "Authorization: Bearer token-for-different-organization" \
http://localhost:3000/api/protected
API リソース検証と組織コンテキストを組み合わせたテストシナリオ:
- 有効な組織 + API スコープ: 組織コンテキストと必要な API スコープの両方を持つトークンでテスト
- API スコープ不足: 組織トークンに必要な API 権限がない場合は 403 Forbidden を期待
- 誤った組織: 別の組織のトークンで API にアクセスした場合は 403 Forbidden を期待
- 誤ったオーディエンス: オーディエンスが組織レベルの API リソースと一致しない場合は 403 Forbidden を期待
# API スコープがない組織トークン - 403 を期待
curl -H "Authorization: Bearer organization-token-without-api-scopes" \
http://localhost:3000/api/protected
さらに詳しく
実践でのロールベースのアクセス制御 (RBAC):アプリケーションのための安全な認可 (Authorization) の実装
マルチテナント SaaS アプリケーションの構築:設計から実装までの完全ガイド