Auth Bypass in Django with Oauth2
Auth Bypass in Django with Oauth2 — how this specific combination creates or exposes the vulnerability
OAuth2 is widely adopted in Django projects to delegate authentication to identity providers (IdPs). When implemented incompletely, the combination of Django’s routing and OAuth2 flows can expose an auth bypass. A common pattern is to protect views with a simple decorator like @login_required while relying on an OAuth2 access token that is validated later or in a different layer. If the view does not independently verify that a valid OAuth2 token has been processed and the user is properly authenticated, an attacker can access the endpoint using an authenticated session that was established without the required OAuth2 context (for example, a locally logged-in session or a session missing the expected claims).
Consider a Django view that expects an OAuth2 access token in the Authorization header but does not enforce token validation before executing business logic. If the token validation is performed lazily or skipped for certain HTTP methods, an authenticated user can reach protected endpoints without satisfying the OAuth2 authorization checks. This can occur when middleware does not enforce token presence, or when the developer assumes that session-based authentication is sufficient after a successful OAuth2 handshake, without checking that the token scopes or issuer match the required permissions.
Another scenario involves improper redirect handling during OAuth2 flows. If the redirect_uri is not strictly validated, an attacker can supply a malicious redirect URI that causes the authorization code or token to be sent to an endpoint they control. After the IdP redirects back, the attacker’s server can exchange the code for tokens, while the victim’s Django application may complete its local login flow with incomplete or mismatched state, leading to an auth bypass where the attacker’s token is accepted indirectly.
Real-world API scans often uncover these issues when endpoints intended to require OAuth2 protection return data without validating the token’s presence, scope, or audience. For example, an endpoint like /api/v1/profile might return user details if a session cookie exists, even when no valid OAuth2 access token is presented. This illustrates why OAuth2 in Django must enforce token validation on every request, verify scopes, and ensure the authentication layer is tightly coupled with the authorization checks.
Oauth2-Specific Remediation in Django — concrete code fixes
To secure Django with OAuth2, validate tokens on each request, enforce scopes, and avoid relying solely on session-based authentication. Use well-audited libraries such as django-oauth-toolkit or integrate IdPs via packages like django-allauth with strict redirect_uri controls. Below are concrete, working examples that demonstrate a secure pattern.
1. Validate OAuth2 access tokens on every request
Create a decorator that ensures a valid OAuth2 access token is present and verified before allowing access to a view. This example uses django-oauth-toolkit to introspect the token and attach the user to the request.
import json
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from oauth2_provider.models import AccessToken
from django.core.exceptions import PermissionDenied
def oauth2_token_required(view_func):
def _wrapped(request, *args, **kwargs):
auth = request.headers.get("Authorization")
if not auth or not auth.startswith("Bearer "):
return JsonResponse({"error": "missing_token", "message": "Authorization header with Bearer token required"}, status=401)
token_string = auth.split(" ")[1]
try:
token = AccessToken.objects.get(token=token_string)
if not token.is_valid():
return JsonResponse({"error": "invalid_token", "message": "Token is expired or revoked"}, status=401)
request.user = token.user
request.scopes = token.scopes.split() if token.scopes else []
except AccessToken.DoesNotExist:
return JsonResponse({"error": "invalid_token", "message": "Token not found"}, status=401)
return view_func(request, *args, **kwargs)
return _wrapped
@require_http_methods(["GET", "POST"])
def profile_view(request):
if not hasattr(request, 'user') or request.user.is_anonymous:
return JsonResponse({"error": "unauthorized"}, status=401)
required_scope = "read:profile"
if required_scope not in request.scopes:
return JsonResponse({"error": "insufficient_scope", "message": f"Scope '{required_scope}' is required"}, status=403)
return JsonResponse({"username": request.user.username, "email": request.user.email})
2. Strict redirect_uri validation for authorization code flow
When initiating OAuth2 flows, validate the redirect_uri against a whitelist of allowed URIs to prevent open redirects and token interception.
from urllib.parse import urlparse, urljoin
from django.conf import settings
def is_safe_redirect_uri(uri, allowed_hosts):
if not uri:
return False
parsed = urlparse(uri)
if parsed.scheme not in ("https",):
return False
if parsed.netloc not in allowed_hosts:
return False
# Ensure no open redirect by resolving relative paths safely
return bool(parsed.path)
REDIRECT_URI_WHITELIST = [
"https://app.example.com/complete/oauth2/",
"https://app.example.com/callback/oauth2/",
]
def initiate_oauth2_flow(request):
allowed = [urljoin(request.build_absolute_uri("/"), u) for u in REDIRECT_URI_WHITELIST]
redirect_uri = request.GET.get("redirect_uri")
if not is_safe_redirect_uri(redirect_uri, [urlparse(u).netloc for u in allowed]):
return JsonResponse({"error": "invalid_redirect_uri"}, status=400)
# Continue with OAuth2 authorization request using validated redirect_uri
return JsonResponse({"redirect": f"https://idp.example.com/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri={redirect_uri}&scope=read:profile"})
3. Enforce scopes and claims in protected views
After token validation, check that the token carries the required scopes and, if applicable, that the issuer (iss) and audience (aud) claims match expectations.
def require_scope(scope):
def decorator(view_func):
def _wrapped(request, *args, **kwargs):
if not hasattr(request, 'scopes') or scope not in request.scopes:
return JsonResponse({"error": "insufficient_scope", "message": f"Scope '{scope}' is required"}, status=403)
return view_func(request, *args, **kwargs)
return _wrapped
return decorator
@require_scope("read:profile")
def profile_scoped_view(request):
return JsonResponse({"data": "protected profile info"})
These examples ensure that OAuth2 tokens are validated on every request, scopes are enforced, and redirect URIs are strictly controlled, reducing the risk of auth bypass in Django applications.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |