Source code for axioms_drf.authentication

"""Django REST Framework authentication classes for JWT token validation.

This module provides authentication classes that integrate with Django REST Framework
to validate OAuth2/OIDC JWT access tokens. It works in conjunction with the
``AccessTokenMiddleware`` to perform token validation.

Configuration:
    Configure safe HTTP methods that bypass authentication in Django settings::

        # Optional: Configure safe HTTP methods (defaults to HEAD and OPTIONS)
        AXIOMS_SAFE_METHODS = ('HEAD', 'OPTIONS', 'GET')

Classes:
    ``HasValidAccessToken``: Main authentication class requiring valid JWT token.
    ``IsAccessTokenAuthenticated``: Alias for ``HasValidAccessToken``.
    ``IsAnyPostOrIsAccessTokenAuthenticated``: Allows POST without authentication.
    ``IsAnyGetOrIsAccessTokenAuthenticated``: Allows GET without authentication.
    ``MissingAuthorizationHeader``: Exception for missing Authorization header.
    ``InvalidAuthorizationBearer``: Exception for invalid Bearer token format.
    ``UnauthorizedAccess``: Exception for invalid or expired tokens.

Example::

    from rest_framework.views import APIView
    from rest_framework.response import Response
    from axioms_drf.authentication import HasValidAccessToken

    class ProtectedView(APIView):
        authentication_classes = [HasValidAccessToken]

        def get(self, request):
            # User is authenticated with valid JWT token
            return Response({'user': request.user})
"""

from django.conf import settings
from rest_framework import authentication, status
from rest_framework.exceptions import APIException


[docs] class HasValidAccessToken(authentication.BaseAuthentication): """Authentication class that validates JWT access tokens. This class integrates with ``AccessTokenMiddleware`` which performs the actual token validation. The middleware sets ``request.auth_jwt`` with the validated token payload, or flags for missing/invalid tokens. The authentication succeeds when: - A valid JWT token is present in the ``Authorization`` header - The token has not expired - The token has valid signature and claims - The token audience matches configured ``AXIOMS_AUDIENCE`` Safe HTTP methods (HEAD, OPTIONS by default) are allowed without authentication to support CORS preflight. Configure ``AXIOMS_SAFE_METHODS`` setting to customize. Raises: MissingAuthorizationHeader: If ``Authorization`` header is not present. InvalidAuthorizationBearer: If Bearer token format is invalid. UnauthorizedAccess: If token is invalid, expired, or has invalid signature. Returns: tuple: (user_identifier, auth_success) where user_identifier is the ``sub`` claim. """
[docs] def authenticate(self, request): """Authenticate the request using JWT token from middleware. Args: request: Django REST Framework request object with ``auth_jwt`` attribute set by ``AccessTokenMiddleware``. Returns: tuple: ``(user_identifier, True)`` if authentication succeeds, where ``user_identifier`` is the subject claim from the token. None: If no authentication is required (safe HTTP methods). Raises: MissingAuthorizationHeader: If ``Authorization`` header is missing. InvalidAuthorizationBearer: If Bearer format is invalid. UnauthorizedAccess: If token validation fails. """ # Allow safe HTTP methods without access token (configurable, defaults to HEAD/OPTIONS) safe_methods = getattr(settings, "AXIOMS_SAFE_METHODS", ("HEAD", "OPTIONS")) if request.method in safe_methods: return (None, True) auth_jwt = request.auth_jwt missing_auth_header = request.missing_auth_header invalid_bearer_token = request.invalid_bearer_token if missing_auth_header is True: raise MissingAuthorizationHeader if invalid_bearer_token is True: raise InvalidAuthorizationBearer if auth_jwt is False: raise UnauthorizedAccess else: if auth_jwt.sub: return (auth_jwt.sub, True) else: raise UnauthorizedAccess return (None, False)
[docs] def authenticate_header(self, request): """Return the ``WWW-Authenticate`` header value for 401 responses. Args: request: Django REST Framework request object. Returns: str: ``WWW-Authenticate`` header value following RFC 6750. """ # Use AXIOMS_ISS_URL or AXIOMS_DOMAIN for realm realm = getattr(settings, "AXIOMS_ISS_URL", None) if not realm and hasattr(settings, "AXIOMS_DOMAIN"): domain = settings.AXIOMS_DOMAIN domain = domain.replace("https://", "").replace("http://", "") realm = f"https://{domain}" return ( "Bearer realm='{}', error='invalid_token', " "error_description='Invalid access token'" ).format(realm or "")
[docs] class IsAccessTokenAuthenticated(HasValidAccessToken): """Alias for ``HasValidAccessToken``. This class provides the same functionality as ``HasValidAccessToken``. Use this if you prefer the naming style. """
[docs] def authenticate(self, request): """Authenticate using parent class implementation. Args: request: Django REST Framework request object. Returns: tuple: Same as ``HasValidAccessToken.authenticate()``. """ super().authenticate(request)
[docs] class IsAnyPostOrIsAccessTokenAuthenticated(HasValidAccessToken): """Authentication class that allows ``POST`` requests without authentication. Useful for public endpoints that accept unauthenticated ``POST`` requests (e.g., user registration, password reset) but require authentication for other methods. Example:: class RegisterView(APIView): authentication_classes = [IsAnyPostOrIsAccessTokenAuthenticated] def post(self, request): # Anyone can register (no auth required) return Response({'status': 'registered'}) def get(self, request): # Requires valid JWT token return Response({'user': request.user}) """
[docs] def authenticate(self, request): """Authenticate request, allowing ``POST`` without token. Args: request: Django REST Framework request object. Returns: tuple: ``(None, True)`` for ``POST`` requests, otherwise delegates to parent. """ # Allow POST requests without access token if request.method == "POST": return (None, True) else: super().authenticate(request)
[docs] class IsAnyGetOrIsAccessTokenAuthenticated(HasValidAccessToken): """Authentication class that allows ``GET`` requests without authentication. Useful for public read endpoints that don't require authentication for viewing but require authentication for modifications. Example:: class ArticleView(APIView): authentication_classes = [IsAnyGetOrIsAccessTokenAuthenticated] def get(self, request): # Anyone can read articles (no auth required) return Response({'articles': []}) def post(self, request): # Requires valid JWT token to create return Response({'status': 'created'}) """
[docs] def authenticate(self, request): """Authenticate request, allowing ``GET`` without token. Args: request: Django REST Framework request object. Returns: tuple: ``(None, True)`` for ``GET`` requests, otherwise delegates to parent. """ # Allow GET requests without access token if request.method == "GET": return (None, True) else: super().authenticate(request)
[docs] class MissingAuthorizationHeader(APIException): """Exception raised when ``Authorization`` header is missing from request. This exception is raised when a protected endpoint is accessed without providing the ``Authorization`` header. Follows RFC 6750 OAuth 2.0 Bearer Token Usage standard. Attributes: status_code: HTTP 401 Unauthorized default_detail: Error message dict with RFC 6750 compliant error and description default_code: ``invalid_token`` """ status_code = status.HTTP_401_UNAUTHORIZED default_detail = { "error": "invalid_token", "error_description": "Missing Authorization Header", } default_code = "invalid_token"
[docs] class InvalidAuthorizationBearer(APIException): """Exception raised when Bearer token format is invalid. This exception is raised when the ``Authorization`` header is present but doesn't follow the ``Bearer <token>`` format. Follows RFC 6750 OAuth 2.0 Bearer Token Usage standard. Attributes: status_code: HTTP 401 Unauthorized default_detail: Error message dict with RFC 6750 compliant error and description default_code: ``invalid_token`` """ status_code = status.HTTP_401_UNAUTHORIZED default_detail = { "error": "invalid_token", "error_description": "Invalid Authorization Bearer", } default_code = "invalid_token"
[docs] class UnauthorizedAccess(APIException): """Exception raised when JWT token validation fails. This exception is raised when: - Token signature is invalid - Token has expired - Token audience doesn't match configured ``AXIOMS_AUDIENCE`` - Token issuer doesn't match configured ``AXIOMS_ISS_URL`` - Token algorithm is not in allowed algorithms - Token is missing required claims Follows RFC 6750 OAuth 2.0 Bearer Token Usage standard. Attributes: status_code: HTTP 401 Unauthorized default_detail: Error message dict with RFC 6750 compliant error and description default_code: ``invalid_token`` """ status_code = status.HTTP_401_UNAUTHORIZED default_detail = { "error": "invalid_token", "error_description": "Invalid access token", } default_code = "invalid_token"