1

So I have this custom authenticator created and I have over 30 endpoints. For all, but 3 endpoints it requires authentication. So I'm pretty much adding @custom_authenticator to every function or @method_decorator(custom_authenticator) in the case of APIView classes. Is there a way I can automatically add this to endpoints and add a decorator that turns off authentication for specific endpoint functions? For example

@donotauth
def endpoint(request)

then endpoint() won't run the authenticator first. The solution should ideally work with the custom authenticator below

custom authenticator

def cognito_authenticator(view_func=None):
    if view_func is None:
        return partial(cognito_authenticator)

    @wraps(view_func)
    def wrapped_view(request, *args, **kwargs):
        # Check the cognito token from the request.
        auth = request.headers.get("Authorization", None)
        if not auth:
            return Response(dict(error='Authorization header expected'), status=status.HTTP_401_UNAUTHORIZED)

        parts = auth.split()

        if parts[0].lower() != "bearer":
            return Response(dict(error='Authorization header must start with bearer'),
                            status=status.HTTP_401_UNAUTHORIZED)
        elif len(parts) == 1:
            return Response(dict(error='Token not found'), status=status.HTTP_401_UNAUTHORIZED)
        elif len(parts) > 2:
            return Response(dict(error='Authorization header must be Bearer token'),
                            status=status.HTTP_401_UNAUTHORIZED)

        token = parts[1]
        try:
            res = decode_cognito_jwt(token)
            expiration = datetime.utcfromtimestamp(res['exp'])
            current_utc = datetime.utcnow()

            if current_utc > expiration:
                return Response(dict(error=f'current time:{current_utc} is after expiration:{expiration}',
                                     user_msg='Please login again'), status=status.HTTP_400_BAD_REQUEST)

        except Exception:
            # Fail if invalid
            return Response(dict(error="Invalid JWT"),
                            status=status.HTTP_401_UNAUTHORIZED)  # Or HttpResponseForbidden()
        else:
            # Proceed with the view if valid
            return view_func(request, *args, **kwargs)

    return wrapped_view

Solution 1 using Middleware:

I tried adding middleware, but it throws an error for anything with @api_view decorator on it. The error I get is AssertionError: .accepted_renderer not set on Response. How do I setup my custom authentication on every endpoint regardless of if it has @api_view decorator or is APIView. The end goal should be to automatically add the above cognito_authenticator to any endpoint and a way to specify when not to use the authenticator (probably a functiond decorator)

View.py

@api_view(['GET'])
@swagger_auto_schema(
    operation_description="Get <count> most recent posts by category"
)
def get_most_recent_posts_by_category(request, category, count):
    return Response(status=status.HTTP_200_OK)

Middleware

from datetime import datetime

from rest_framework import status
from rest_framework.response import Response

from cheers.core.api.jwt_helpers import decode_cognito_jwt


class CognitoMiddleware(object):
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        auth = request.headers.get("Authorization", None)
        if not auth:
            return Response(dict(error='Authorization header expected'), status=status.HTTP_401_UNAUTHORIZED)

        parts = auth.split()

        if parts[0].lower() != "bearer":
            return Response(dict(error='Authorization header must start with bearer'),
                            status=status.HTTP_401_UNAUTHORIZED)
        elif len(parts) == 1:
            return Response(dict(error='Token not found'), status=status.HTTP_401_UNAUTHORIZED)
        elif len(parts) > 2:
            return Response(dict(error='Authorization header must be Bearer token'),
                            status=status.HTTP_401_UNAUTHORIZED)

        token = parts[1]
        try:
            res = decode_cognito_jwt(token)
            expiration = datetime.utcfromtimestamp(res['exp'])
            current_utc = datetime.utcnow()

            if current_utc > expiration:
                return Response(dict(error=f'current time:{current_utc} is after expiration:{expiration}',
                                     user_msg='Please login again'), status=status.HTTP_400_BAD_REQUEST)

        except Exception:
            # Fail if invalid
            return Response(dict(error="Invalid JWT"),
                            status=status.HTTP_401_UNAUTHORIZED)  # Or HttpResponseForbidden()
        else:
            # Proceed with the view if valid
            return None

settings.py

MIDDLEWARE = [
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'cheers.middleware.CognitoMiddleware.CognitoMiddleware'
]

Solution 2 using authenticator

authenticator.py

class CognitoAuthentication(BaseAuthentication):
    def authenticate(self, request):
        auth = request.headers.get("Authorization", None)
        if not auth:
            return Response(dict(error='Authorization header expected'), status=status.HTTP_401_UNAUTHORIZED)

        parts = auth.split()

        if parts[0].lower() != "bearer":
            return Response(dict(error='Authorization header must start with bearer'),
                            status=status.HTTP_401_UNAUTHORIZED)
        elif len(parts) == 1:
            return Response(dict(error='Token not found'), status=status.HTTP_401_UNAUTHORIZED)
        elif len(parts) > 2:
            return Response(dict(error='Authorization header must be Bearer token'),
                            status=status.HTTP_401_UNAUTHORIZED)

        token = parts[1]
        try:
            res = decode_cognito_jwt(token)
            expiration = datetime.utcfromtimestamp(res['exp'])
            current_utc = datetime.utcnow()

            if current_utc > expiration:
                return Response(dict(error=f'current time:{current_utc} is after expiration:{expiration}',
                                     user_msg='Please login again'), status=status.HTTP_400_BAD_REQUEST)

        except Exception:
            # Fail if invalid
            return Response(dict(error="Invalid JWT"),
                            status=status.HTTP_401_UNAUTHORIZED)  # Or HttpResponseForbidden()
        else:
            # Proceed with the view if valid
            return AnonymousUser(), None

settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'cheers.utils.authenticator.CognitoAuthentication',
    ),
}

but on an APIView post function it gives the error django.template.response.ContentNotRenderedError: The response content must be rendered before it can be iterated over.

Brian Destura
  • 11,487
  • 3
  • 18
  • 34
  • That would be a [middleware](https://docs.djangoproject.com/en/3.2/topics/http/middleware/). – Selcuk Sep 27 '21 at 04:38

2 Answers2

2

To add different level authentication in DRF API endpoint, You can use project level authentication coupled with view level authentication :

# config/settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
    # 'rest_framework.permissions.AllowAny', to allow all
    # 'rest_framework.permissions.IsAdminUser', only admin
    # 'rest_framework.permissions.IsAuthenticatedOrReadOnly', only authenticated can write
    'rest_framework.permissions.IsAuthenticated',
    ]
}

# app_name/views.py
@permission_classes((IsAdminUser, ))
def example_view(request, format=None):
    content = {
        'status': 'request was permitted'
    }
    return Response(content)

In this case all endpoints are protected by the IsAuthenticated permission. But the view example_view override the global permission and use IsAdminUser instead.

Rvector
  • 2,312
  • 1
  • 8
  • 17
1

SOLUTION 1 (DRF Custom Authentication classes approach)

I've already answered your similar question here which uses the DRF authentication class approach. That approach will apply your authentication to all APIViews by default, and you can also specify which views don't need to use that authentication. This I think should solve all your your problems here.

If you decide on the DRF authentication class approach, you can disable authentication on a view by setting the authentication_classes to blank on a function-based view like this:

from rest_framework.decorators import api_view, authentication_classes, permission_classes


@api_view(['GET'])
@authentication_classes([])
@permission_classes([])
def view(request):
    return Response({"message": "Hello for today! See you tomorrow!"})

For class-based views:

class SomeView(APIView):
    authentication_classes = ()
    permission_classes = ()

Similar thing for permission_classes. The above example effectively means there is no security on the views! So don't forget to add the authentication and permission classes you want, for example:

@api_view(['GET'])
@authentication_classes([BasicAuthentication, ])
def view(request):
    return Response({"message": "Hello for today! See you tomorrow!"})


class SomeView(APIView):
    authentication_classes = (BasicAuthentication, )

SOLUTION 2 (MIDDLEWARE)

But if you really want to use a middleware, I would suggest to just use JsonResponse instead of DRF's Response. This is because Response's .accepted_renderer is processed by the DRF's APIView class or DRF's @api_view decorator.

Since your middleware does not do that processing, you get the error. You might be tempted to move/use that processing to your middleware, but it is unnecessary IMO.

So your middleware can be:

from rest_framework import status
from django.http import JsonResponse

from cheers.core.api.jwt_helpers import decode_cognito_jwt


class CognitoMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        auth = request.headers.get("Authorization", None)
        if not auth:
            return JsonResponse(dict(error='Authorization header expected'), status=status.HTTP_401_UNAUTHORIZED)

        parts = auth.split()

        if parts[0].lower() != "bearer":
            return JsonResponse(
                dict(error='Authorization header must start with bearer'), status=status.HTTP_401_UNAUTHORIZED
            )

        elif len(parts) == 1:
            return JsonResponse(dict(error='Token not found'), status=status.HTTP_401_UNAUTHORIZED)

        elif len(parts) > 2:
            return JsonResponse(
                dict(error='Authorization header must be Bearer token'), status=status.HTTP_401_UNAUTHORIZED
            )

        token = parts[1]

        try:
            res = decode_cognito_jwt(token)
            expiration = datetime.utcfromtimestamp(res['exp'])
            current_utc = datetime.utcnow()

            if current_utc > expiration:
                return JsonResponse(
                    dict(error=f'current time:{current_utc} is after expiration:{expiration}',
                    user_msg='Please login again'), status=status.HTTP_400_BAD_REQUEST
                )

        except Exception:
            # Fail if invalid
            return JsonResponse(
                dict(error="Invalid JWT"),
                status=status.HTTP_401_UNAUTHORIZED
            )  # Or HttpResponseForbidden()

        else:
            # Proceed with the view if valid
            return None

If you want to specify which views/urls you want this middleware to ignore, some things you can do:

  1. Specify a separate reference for this middleware to use, for example a setting that lists urls that this middleware will ignore
  2. Add some sort of flag to your view for this middleware to check whether to ignore a view or not

EDIT:

With regards to the 403 being returned on the DRF approach, it's explained here. One way to ensure 401 is returned is by returning Bearer in authenticate_header() like this:

class MyCustomAuthentication(BaseAuthentication):
    def authenticate(self, request):
        ...
    
    def authenticate_header(self, request):
        return 'Bearer'
Brian Destura
  • 11,487
  • 3
  • 18
  • 34