HIGH auth bypassdjangooauth2

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 IDNameSeverity
CWE-287Improper Authentication CRITICAL
CWE-306Missing Authentication for Critical Function CRITICAL
CWE-307Brute Force HIGH
CWE-308Single-Factor Authentication MEDIUM
CWE-309Use of Password System for Primary Authentication MEDIUM
CWE-347Improper Verification of Cryptographic Signature HIGH
CWE-384Session Fixation HIGH
CWE-521Weak Password Requirements MEDIUM
CWE-613Insufficient Session Expiration MEDIUM
CWE-640Weak Password Recovery HIGH

Frequently Asked Questions

Why does relying on @login_required alongside OAuth2 increase auth bypass risk?
@login_required checks session-based authentication, not OAuth2 token validity. If token validation is deferred or skipped, an attacker can reach protected endpoints using a local session without satisfying OAuth2 requirements, leading to auth bypass.
How can I prevent open redirect abuse in OAuth2 flows in Django?
Validate redirect_uri against a strict whitelist, enforce HTTPS, and resolve relative paths safely before using it in authorization requests. Never trust user-supplied redirect_uri without server-side verification.