Django OAuth Toolkit

authentication libraryDjango OAuth ToolkitOAuth2DjangoPythonAPI authenticationauthorization serverresource serveroauthlibRFC compliant

Authentication Library

Django OAuth Toolkit

Overview

Django OAuth Toolkit is a comprehensive official toolkit for building OAuth2 servers with Django. As of 2025, version 3.0.1 is the latest release, providing a complete RFC 6749-compliant OAuth2 implementation. Built on the excellent OAuthLib library with Django integration, it supports all major grant types including authorization code, client credentials, implicit, and refresh token flows. With full Django REST Framework support, scope-based access control, and customizable token management, it enables building enterprise-grade OAuth2 authorization and resource servers.

Details

Django OAuth Toolkit is a library that fully integrates OAuthLib's advanced capabilities with the Django ecosystem. It automates the entire OAuth2 workflow including application management, client authentication, token issuance, scope management, and user authorization, allowing developers to focus on business logic rather than authentication logic. It provides flexibility to meet enterprise requirements through OAuth2 application configuration from the admin interface, fine-grained scope control, custom authentication classes, and security policy settings. Supporting Django 3.2+ and Python 3.8+, it's optimized for modern Django development environments.

Key Features

  • Full RFC 6749 Compliance: Implementation adhering to all OAuth2 standard specifications
  • All Grant Types Support: Authorization Code, Client Credentials, Implicit, ROPC support
  • Django Integration: Complete integration with Django's authentication system and admin interface
  • DRF Support: Authentication and authorization features for Django REST Framework
  • Scope Management: Fine-grained access control and resource protection
  • Customizable: Flexible configuration and customization according to business requirements

Advantages and Disadvantages

Advantages

  • Established as the standard OAuth2 server implementation for Django projects
  • Robust and reliable RFC-compliant implementation based on OAuthLib
  • Easy OAuth2 application management through Django admin interface
  • Perfect integration with Django REST Framework and API protection features
  • Easy learning with rich documentation and community support
  • Stability and scalability with extensive enterprise environment track record

Disadvantages

  • Django-specific, unusable with other frameworks
  • High learning cost for beginners due to OAuth2 complexity
  • Performance tuning required for large-scale services
  • Limited support for latest specifications like OpenID Connect
  • Configuration difficulty for complex customization requirements
  • Operational overhead for token management and security policies

Reference Pages

Usage Examples

Installation and Project Setup

# Install Django OAuth Toolkit
pip install django-oauth-toolkit

# For use with Django REST Framework
pip install djangorestframework

# For PostgreSQL (recommended)
pip install psycopg2-binary

# Development dependencies
pip install python-dotenv
# settings.py - Django Configuration
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# Django OAuth Toolkit Configuration
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 (for API server)
    'rest_framework',
    
    # Applications
    'accounts',
    'api',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'oauth2_provider.middleware.OAuth2TokenMiddleware',  # OAuth2 middleware
    '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'

# Database Configuration
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 Configuration
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 Configuration
OAUTH2_PROVIDER = {
    # Token Settings
    'ACCESS_TOKEN_EXPIRE_SECONDS': 3600,           # 1 hour
    'REFRESH_TOKEN_EXPIRE_SECONDS': 3600 * 24 * 7, # 7 days
    
    # Security Settings
    'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600,      # 10 minutes
    'ROTATE_REFRESH_TOKEN': True,                  # Refresh token rotation
    
    # Scope Settings
    'SCOPES': {
        'read': 'Read scope',
        'write': 'Write scope',
        'admin': 'Admin scope',
        'profile': 'Profile access',
        'email': 'Email access',
    },
    'DEFAULT_SCOPES': ['read'],
    
    # Application Settings
    'APPLICATION_MODEL': 'oauth2_provider.Application',
    
    # PKCE Configuration (recommended)
    'PKCE_REQUIRED': True,
    
    # Security Headers
    'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.JSONOAuthLibCore',
}

# Security Settings
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 Configuration (for frontend applications)
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",  # React development server
    "http://127.0.0.1:3000",
]

CORS_ALLOW_CREDENTIALS = True

# Logging Configuration
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 Configuration and Endpoints

# urls.py - Main URL Configuration
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    
    # OAuth2 Endpoints
    path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
    
    # API Endpoints
    path('api/v1/', include('api.urls')),
    
    # Authentication Related
    path('auth/', include('accounts.urls')),
]

# api/urls.py - API URL Configuration
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 Application Management

# management/commands/create_oauth_app.py - OAuth2 Application Creation Command
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,  # System application
            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 - User Profile Extension
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 Authentication and Scope Control

# api/views.py - OAuth2 Protected API Views
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):
    """User Management API (Admin privileges required)"""
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated, TokenHasScope]
    required_scopes = ['admin']  # Admin scope required

    def get_queryset(self):
        # Queryset restriction based on scope
        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):
    """Post Management API"""
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticated, TokenHasScope]
    
    def get_required_scopes(self):
        # Scope requirements based on action
        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):
        """User's own posts list"""
        posts = Post.objects.filter(author=request.user)
        serializer = self.get_serializer(posts, many=True)
        return Response(serializer.data)

class UserProfileView(APIView):
    """User Profile 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):
        # Profile updates also require write scope
        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):
    """Scope-based Access Control Demo"""
    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)

Authentication Flow Implementation

# accounts/views.py - Authentication Flow Management
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 Authentication Flow Start Page"""
    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):
    """User Consent Confirmation Page"""
    if request.method == 'POST':
        # Continue OAuth2 flow when user consents
        return redirect('/o/authorize/?' + request.META['QUERY_STRING'])
    
    return render(request, 'oauth2/consent.html')

@require_http_methods(["POST"])
def revoke_token_view(request):
    """Token Revocation 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):
    """Decorator-based API Protection"""
    return JsonResponse({
        'message': 'This is a protected resource',
        'user_id': request.user.id,
        'username': request.user.username,
    })

Client-side Authentication Implementation

# oauth_client.py - OAuth2 Client Implementation Example
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):
        """Generate Authorization 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):
        """Exchange Authorization Code for Token"""
        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):
        """Refresh Access 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):
        """Get Token via Client Credentials Grant"""
        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 Request with Authentication Header"""
        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

# Usage Example
if __name__ == "__main__":
    client = OAuth2Client(
        client_id='your-client-id',
        client_secret='your-client-secret',
        base_url='http://localhost:8000'
    )
    
    # Generate authorization URL
    auth_url = client.get_authorization_url(
        redirect_uri='http://localhost:3000/callback',
        scope='read write profile'
    )
    print(f"Authorization URL: {auth_url}")
    
    # Token exchange (after authentication)
    # token_data = client.exchange_code_for_token(
    #     code='received-auth-code',
    #     redirect_uri='http://localhost:3000/callback'
    # )
    
    # API access
    # response = client.make_authenticated_request(
    #     access_token=token_data['access_token'],
    #     url='http://localhost:8000/api/v1/profile/'
    # )

Custom Authentication Backend and Templates

<!-- templates/oauth2/authorize.html - Authorization Consent Page -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Authorization Request - {{ 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">Authorization Request</h4>
                    </div>
                    <div class="card-body">
                        <div class="text-center mb-4">
                            <h5>{{ application.name }}</h5>
                            <p class="text-muted">is requesting access to your account</p>
                        </div>
                        
                        {% if scopes %}
                        <div class="mb-4">
                            <h6>Requested permissions:</h6>
                            <ul class="list-group">
                                {% for scope in scopes %}
                                <li class="list-group-item">
                                    <strong>{{ scope }}</strong>
                                    {% if scope == 'read' %}
                                        - Read basic information
                                    {% elif scope == 'write' %}
                                        - Create and update data
                                    {% elif scope == 'profile' %}
                                        - Access profile information
                                    {% elif scope == 'email' %}
                                        - Access email address
                                    {% elif scope == 'admin' %}
                                        - Administrator privileges
                                    {% 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">
                                    Allow Access
                                </button>
                                <button type="submit" name="deny" class="btn btn-outline-secondary">
                                    Deny
                                </button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>