Django OAuth Toolkit
認証ライブラリ
Django OAuth Toolkit
概要
Django OAuth Toolkitは、DjangoでOAuth2サーバーを構築するための包括的な公式ツールキットです。2025年現在、バージョン3.0.1が最新リリースとなり、RFC 6749完全準拠のOAuth2実装を提供します。優れたOAuthLibライブラリをベースにDjango統合を実現し、認証コード、クライアントクレデンシャル、インプリシット、リフレッシュトークン等の全主要グラントタイプをサポートします。Django REST Framework完全対応、スコープベースアクセス制御、カスタマイズ可能なトークン管理により、エンタープライズグレードのOAuth2認証サーバーとリソースサーバーを構築できます。
詳細
Django OAuth Toolkitは、OAuthLibの高度な機能をDjangoエコシステムと完全統合したライブラリです。アプリケーション管理、クライアント認証、トークン発行、スコープ管理、ユーザー承認等のOAuth2ワークフロー全体を自動化し、開発者は認証ロジックではなくビジネスロジックに集中できます。管理画面からのOAuth2アプリケーション設定、細かなスコープ制御、カスタム認証クラス、セキュリティポリシー設定等の企業要件に対応する柔軟性を提供します。Django 3.2以降とPython 3.8以降をサポートし、モダンなDjango開発環境に最適化されています。
主な特徴
- RFC 6749完全準拠: すべてのOAuth2標準仕様に準拠した実装
- 全グラントタイプサポート: Authorization Code、Client Credentials、Implicit、ROPC対応
- Django統合: Djangoの認証システムと管理画面の完全統合
- DRF対応: Django REST Frameworkでの認証・認可機能
- スコープ管理: 細かなアクセス制御とリソース保護
- カスタマイズ可能: ビジネス要件に応じた柔軟な設定とカスタマイズ
メリット・デメリット
メリット
- DjangoプロジェクトでOAuth2サーバーの標準実装として確立
- OAuthLibベースによる堅牢で信頼性の高いRFC準拠実装
- Django管理画面でのOAuth2アプリケーション管理の簡単さ
- Django REST Frameworkとの完璧な統合とAPI保護機能
- 豊富なドキュメントとコミュニティサポートによる学習容易性
- エンタープライズ環境での実績豊富な安定性と拡張性
デメリット
- Django専用のため他のフレームワークでは利用不可
- OAuth2の複雑さにより初学者には学習コストが高い
- 大規模サービスでのパフォーマンスチューニングが必要
- OpenID Connect等の最新仕様への対応が限定的
- カスタマイズ要件が複雑な場合の設定の難しさ
- トークン管理とセキュリティポリシーの運用負荷
参考ページ
書き方の例
インストールとプロジェクト設定
# Django OAuth Toolkitのインストール
pip install django-oauth-toolkit
# Django REST Frameworkと併用する場合
pip install djangorestframework
# PostgreSQL使用時(推奨)
pip install psycopg2-binary
# 開発時の依存関係
pip install python-dotenv
# settings.py - Django設定
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# Django OAuth Toolkit設定
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# OAuth2 Provider
'oauth2_provider',
# Django REST Framework(APIサーバー用)
'rest_framework',
# アプリケーション
'accounts',
'api',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'oauth2_provider.middleware.OAuth2TokenMiddleware', # OAuth2ミドルウェア
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'project.urls'
# データベース設定
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DATABASE_NAME', 'oauth_server'),
'USER': os.getenv('DATABASE_USER', 'postgres'),
'PASSWORD': os.getenv('DATABASE_PASSWORD', ''),
'HOST': os.getenv('DATABASE_HOST', 'localhost'),
'PORT': os.getenv('DATABASE_PORT', '5432'),
}
}
# Django REST Framework設定
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20
}
# OAuth2 設定
OAUTH2_PROVIDER = {
# トークン設定
'ACCESS_TOKEN_EXPIRE_SECONDS': 3600, # 1時間
'REFRESH_TOKEN_EXPIRE_SECONDS': 3600 * 24 * 7, # 7日間
# セキュリティ設定
'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, # 10分
'ROTATE_REFRESH_TOKEN': True, # リフレッシュトークンローテーション
# スコープ設定
'SCOPES': {
'read': 'Read scope',
'write': 'Write scope',
'admin': 'Admin scope',
'profile': 'Profile access',
'email': 'Email access',
},
'DEFAULT_SCOPES': ['read'],
# アプリケーション設定
'APPLICATION_MODEL': 'oauth2_provider.Application',
# PKCE設定(推奨)
'PKCE_REQUIRED': True,
# セキュリティヘッダー
'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.JSONOAuthLibCore',
}
# セキュリティ設定
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_SSL_REDIRECT = not DEBUG
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
# CORS設定(フロントエンドアプリケーション用)
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # React開発サーバー
"http://127.0.0.1:3000",
]
CORS_ALLOW_CREDENTIALS = True
# ログ設定
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': 'oauth2.log',
},
},
'loggers': {
'oauth2_provider': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}
URL設定とエンドポイント
# urls.py - メインURL設定
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# OAuth2エンドポイント
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
# API エンドポイント
path('api/v1/', include('api.urls')),
# 認証関連
path('auth/', include('accounts.urls')),
]
# api/urls.py - API URL設定
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'posts', views.PostViewSet)
urlpatterns = [
path('', include(router.urls)),
path('profile/', views.UserProfileView.as_view(), name='user-profile'),
path('protected/', views.ProtectedView.as_view(), name='protected'),
]
OAuth2アプリケーション管理
# management/commands/create_oauth_app.py - OAuth2アプリケーション作成コマンド
from django.core.management.base import BaseCommand
from oauth2_provider.models import Application
class Command(BaseCommand):
help = 'Create OAuth2 application'
def add_arguments(self, parser):
parser.add_argument('--name', type=str, help='Application name')
parser.add_argument('--client-type', type=str, default='confidential',
help='Client type (confidential or public)')
parser.add_argument('--grant-type', type=str, default='authorization-code',
help='Grant type')
def handle(self, *args, **options):
application = Application.objects.create(
name=options['name'] or "My OAuth2 App",
user=None, # システムアプリケーション
client_type=Application.CLIENT_CONFIDENTIAL
if options['client_type'] == 'confidential'
else Application.CLIENT_PUBLIC,
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
)
self.stdout.write(
self.style.SUCCESS(
f'Successfully created application:\n'
f'Client ID: {application.client_id}\n'
f'Client Secret: {application.client_secret}\n'
f'Name: {application.name}'
)
)
# accounts/models.py - ユーザープロファイル拡張
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
email = models.EmailField(unique=True)
phone_number = models.CharField(max_length=20, blank=True)
birth_date = models.DateField(null=True, blank=True)
profile_image = models.ImageField(upload_to='profiles/', blank=True)
is_verified = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=30, blank=True)
website = models.URLField(blank=True)
organization = models.CharField(max_length=100, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
API認証とスコープ制御
# api/views.py - OAuth2で保護されたAPIビュー
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView
from oauth2_provider.contrib.rest_framework import TokenHasScope
from django.contrib.auth import get_user_model
from .serializers import UserSerializer, PostSerializer
from .models import Post
User = get_user_model()
class UserViewSet(viewsets.ModelViewSet):
"""ユーザー管理API(管理者権限必要)"""
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated, TokenHasScope]
required_scopes = ['admin'] # 管理者スコープ必須
def get_queryset(self):
# スコープに応じたクエリセット制限
if self.request.auth.scope.has_scope('admin'):
return User.objects.all()
return User.objects.filter(id=self.request.user.id)
class PostViewSet(viewsets.ModelViewSet):
"""投稿管理API"""
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated, TokenHasScope]
def get_required_scopes(self):
# アクションに応じたスコープ要件
if self.action in ['create', 'update', 'partial_update', 'destroy']:
return ['write']
return ['read']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=False, methods=['get'])
def my_posts(self, request):
"""ユーザー自身の投稿一覧"""
posts = Post.objects.filter(author=request.user)
serializer = self.get_serializer(posts, many=True)
return Response(serializer.data)
class UserProfileView(APIView):
"""ユーザープロファイルAPI"""
permission_classes = [permissions.IsAuthenticated, TokenHasScope]
required_scopes = ['profile']
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
def put(self, request):
# プロファイル更新にはwriteスコープも必要
if not request.auth.scope.has_scope('write'):
return Response(
{'error': 'Write scope required for profile updates'},
status=status.HTTP_403_FORBIDDEN
)
serializer = UserSerializer(request.user, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ProtectedView(APIView):
"""スコープベースアクセス制御のデモ"""
permission_classes = [permissions.IsAuthenticated, TokenHasScope]
required_scopes = ['read']
def get(self, request):
content = {
'message': 'Hello, OAuth2 World!',
'user': request.user.username,
'scopes': list(request.auth.scope),
'application': request.auth.application.name,
}
return Response(content)
認証フロー実装
# accounts/views.py - 認証フロー管理
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from oauth2_provider.models import Application, AccessToken
from oauth2_provider.decorators import protected_resource
import json
def authorize_view(request):
"""OAuth2認証フロー開始ページ"""
client_id = request.GET.get('client_id')
redirect_uri = request.GET.get('redirect_uri')
scope = request.GET.get('scope', '')
state = request.GET.get('state', '')
try:
application = Application.objects.get(client_id=client_id)
except Application.DoesNotExist:
return render(request, 'oauth2/error.html', {
'error': 'Invalid client_id'
})
context = {
'application': application,
'client_id': client_id,
'redirect_uri': redirect_uri,
'scope': scope,
'state': state,
'scopes': scope.split(' ') if scope else [],
}
return render(request, 'oauth2/authorize.html', context)
@login_required
def user_consent_view(request):
"""ユーザー同意確認ページ"""
if request.method == 'POST':
# ユーザーが同意した場合のOAuth2フロー継続
return redirect('/o/authorize/?' + request.META['QUERY_STRING'])
return render(request, 'oauth2/consent.html')
@require_http_methods(["POST"])
def revoke_token_view(request):
"""トークン無効化API"""
try:
data = json.loads(request.body)
token = data.get('token')
if not token:
return JsonResponse({'error': 'Token required'}, status=400)
try:
access_token = AccessToken.objects.get(token=token)
access_token.delete()
return JsonResponse({'message': 'Token revoked successfully'})
except AccessToken.DoesNotExist:
return JsonResponse({'error': 'Invalid token'}, status=400)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
@protected_resource(scopes=['profile'])
def protected_api_view(request):
"""デコレーターベースのAPI保護"""
return JsonResponse({
'message': 'This is a protected resource',
'user_id': request.user.id,
'username': request.user.username,
})
クライアント側の認証実装
# oauth_client.py - OAuth2クライアント実装例
import requests
import urllib.parse
from django.conf import settings
class OAuth2Client:
def __init__(self, client_id, client_secret, base_url):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
self.token_url = f"{base_url}/o/token/"
self.authorize_url = f"{base_url}/o/authorize/"
def get_authorization_url(self, redirect_uri, scope=None, state=None):
"""認証URL生成"""
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': redirect_uri,
}
if scope:
params['scope'] = scope
if state:
params['state'] = state
return f"{self.authorize_url}?{urllib.parse.urlencode(params)}"
def exchange_code_for_token(self, code, redirect_uri):
"""認証コードをトークンに交換"""
data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': redirect_uri,
'client_id': self.client_id,
'client_secret': self.client_secret,
}
response = requests.post(self.token_url, data=data)
response.raise_for_status()
return response.json()
def refresh_access_token(self, refresh_token):
"""アクセストークンのリフレッシュ"""
data = {
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
'client_id': self.client_id,
'client_secret': self.client_secret,
}
response = requests.post(self.token_url, data=data)
response.raise_for_status()
return response.json()
def get_client_credentials_token(self, scope=None):
"""クライアントクレデンシャルグラントでトークン取得"""
data = {
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret,
}
if scope:
data['scope'] = scope
response = requests.post(self.token_url, data=data)
response.raise_for_status()
return response.json()
def make_authenticated_request(self, access_token, url, method='GET', data=None):
"""認証ヘッダー付きAPIリクエスト"""
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
}
if method.upper() == 'GET':
response = requests.get(url, headers=headers)
elif method.upper() == 'POST':
response = requests.post(url, json=data, headers=headers)
elif method.upper() == 'PUT':
response = requests.put(url, json=data, headers=headers)
elif method.upper() == 'DELETE':
response = requests.delete(url, headers=headers)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
return response
# 使用例
if __name__ == "__main__":
client = OAuth2Client(
client_id='your-client-id',
client_secret='your-client-secret',
base_url='http://localhost:8000'
)
# 認証URL生成
auth_url = client.get_authorization_url(
redirect_uri='http://localhost:3000/callback',
scope='read write profile'
)
print(f"認証URL: {auth_url}")
# トークン交換(認証後)
# token_data = client.exchange_code_for_token(
# code='received-auth-code',
# redirect_uri='http://localhost:3000/callback'
# )
# APIアクセス
# response = client.make_authenticated_request(
# access_token=token_data['access_token'],
# url='http://localhost:8000/api/v1/profile/'
# )
カスタム認証バックエンドとテンプレート
<!-- templates/oauth2/authorize.html - 認証同意ページ -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>認証リクエスト - {{ application.name }}</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">認証リクエスト</h4>
</div>
<div class="card-body">
<div class="text-center mb-4">
<h5>{{ application.name }}</h5>
<p class="text-muted">があなたのアカウントへのアクセスを要求しています</p>
</div>
{% if scopes %}
<div class="mb-4">
<h6>要求されるアクセス権限:</h6>
<ul class="list-group">
{% for scope in scopes %}
<li class="list-group-item">
<strong>{{ scope }}</strong>
{% if scope == 'read' %}
- 基本情報の読み取り
{% elif scope == 'write' %}
- データの作成・更新
{% elif scope == 'profile' %}
- プロファイル情報へのアクセス
{% elif scope == 'email' %}
- メールアドレスへのアクセス
{% elif scope == 'admin' %}
- 管理者権限
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post" action="/o/authorize/">
{% csrf_token %}
<input type="hidden" name="client_id" value="{{ client_id }}">
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
<input type="hidden" name="scope" value="{{ scope }}">
<input type="hidden" name="state" value="{{ state }}">
<input type="hidden" name="response_type" value="code">
<div class="d-grid gap-2">
<button type="submit" name="allow" class="btn btn-primary">
アクセスを許可
</button>
<button type="submit" name="deny" class="btn btn-outline-secondary">
拒否
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>