4

I'm using Python 3.9 with Django 3. I have defined this middleware ...

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'directory.middleware.extend_token_response.ExtendTokenResponse'
]

However, I don't want the middleware to apply to a certain URL. I have hard-coded this in the middleware like so

class ExtendTokenResponse:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        response = self.get_response(request)
        if request.path != '/' + LOGOUT_PATH:
            # Code to be executed for each request before
            # the view (and later middleware) are called.
            is_expired = True
            try:
                token = request.auth
                print("req path: %s" % request.path)
                is_expired = is_token_expired(token) if token else True
            except Exception as err:
                print(err)
            if not is_expired:

but this seems a little sloppy and I would think the middleware comes with somethign out of the box to configure that this wouldn't need to be applied to my "/logout" path. Is there a more elegant way to configure this?

Edit: In response to Bernhard Vallant's answer, I changed my middleware to the below

def token_response_exempt(view_func):
    # Set an attribute on the function to mark it as exempt
    def wrapped_view(*args, **kwargs):
        return view_func(*args, **kwargs)

    wrapped_view.token_response_exempt = True
    return wraps(view_func)(wrapped_view)

class ExtendTokenResponse: def init(self, get_response): self.get_response = get_response # One-time configuration and initialization.

def process_view(self, request, view_func, view_args, view_kwargs):
    print("in process view method ...\n")
    if getattr(view_func, "token_response_exempt", False):
        print("returning none ...\n")
        return None
    # Code to be executed for each request before
    # the view (and later middleware) are called.
    is_expired = True
    try:
        token = request.auth
        print("req path: %s" % request.path)
        is_expired = is_token_expired(token) if token else True
    except Exception as err:
        print(err)
    if not is_expired:
        token.delete()
        new_token = Token.objects.create(user = token.user)

        # Code to be executed for each request/response after
        # the view is called.
        print("setting new token to %s" % new_token)
        request.token = new_token 

def __call__(self, request):
    response = self.get_response(request)
    print("---- in call method ----\n")
    if getattr(request, "token", None) is not None:
        print("setting refresh token header = %s" % request.token)
        response['Refresh-Token'] = request.token
    return response

but any call to an endpoint, e.g.,

curl --header "Content-type: application/json" --data "$req" --request POST "http://localhost:8000/login"

results in no token being retrieved from the reqeust. "request.auth" generates the error

'WSGIRequest' object has no attribute 'auth'
Dave
  • 15,639
  • 133
  • 442
  • 830
  • add an `if request.user.is_authenticated:` – amd Jun 08 '22 at 23:39
  • I don't think that's going to work in our JWT authentication scheme. – Dave Jun 09 '22 at 00:04
  • You could also add a list on the class like `skip_urls = [reverse("my_logout_url"), ... ]` then in the middleware just check `request.path in self.skip_urls`. I don't think there is an out of the box support by django to ignore certain urls on a middleware, since middleware classes (if classes are used) themselves are just plain python classes – Brian Destura Jun 16 '22 at 23:32

2 Answers2

5

Django itself doesn't provide a solution for this. Probably hardcoding/defining paths in your settings/middleware is fine as long it is a middleware that primarly exists for one specific project.

However if you want to mark certain views to exclude them from being processed you could use decorators in the same way Django does with the csrf_exempt decorator.

from functools import wraps

def token_response_exempt(view_func):
    # Set an attribute on the function to mark it as exempt
    def wrapped_view(*args, **kwargs):
        return view_func(*args, **kwargs)

    wrapped_view.token_response_exempt = True
    return wraps(view_func)(wrapped_view)

# your middleware
class ExtendTokenResponse:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        if getattr(request, "token", None) is not None:
            response['Refresh-Token'] = request.token
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
         if getattr(view_func, "token_response_exempt", False):
            return None
         # do your token generation here
         request.token = token

And then you can use decorator like the following:

# urls.py

urlpatterns = [
    path('logout/', token_response_exempt(LogOutView.as_view())),
]
Bernhard Vallant
  • 49,468
  • 20
  • 120
  • 148
  • Thanks for this. Would the "process_view" method you have defined in the class replace my "__call__" method or is it a different method entirely? – Dave Jun 22 '22 at 14:42
  • It is not exactly the same but as [the documentation](https://docs.djangoproject.com/en/4.0/topics/http/middleware/#process-view) says it gets executed before a view is called (which seems to be what you want as you "want to execute code before the view", but it also gives you the possibility to get access to the `view_func` that is going to be processed so that you can check if the decorator set any attributes on it. – Bernhard Vallant Jun 22 '22 at 17:40
  • I edited my quesiton with what I think you were describing. Now I get the error "TypeError: 'ExtendTokenResponse' object is not callable" on any call. Guess I'm a little slow on the uptake – Dave Jun 22 '22 at 21:36
  • If you need to access the response as well you need to do that in the `__call__()` method. I've updated the answer! Furthermore the first argument `self` to `process_view` somehow got lost :( – Bernhard Vallant Jun 23 '22 at 08:46
  • I moved the token check/generation logic to the "process_view" method per your suggestion but something is still not quite right. For authenticated endpoints, the "token = request.auth" line generates a "'WSGIRequest' object has no attribute 'auth'" error. Updated my question with the exact code I used. – Dave Jun 23 '22 at 14:31
0

About your case, I have 2 recommendations below:

Method 1: use process_view and define a list func will be excluded with structure "app.module.func" and check to skip in process_view

# In settings.py
EXCLUDE_FROM_MY_MIDDLEWARE =set({'custom_app.views.About'})

# In middlewares.py
class ExtendTokenResponse:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        logger.info(f'request hit request {request}')
        response = self.get_response(request)

        # Code to be executed for each request/response after
        # the view is called.

        return response

    def process_view(self, request, view_func, view_args, view_kwargs):

        view_function = '.'.join((view_func.__module__, view_func.__name__))
        exclusion_set=getattr(settings,'EXCLUDE_FROM_MY_MIDDLEWARE',set() )
        if view_function in exclusion_set:
            return None

Method 2: Use decorator_from_middleware and apply middleware to each function needed it.

from django.utils.decorators import decorator_from_middleware
# with function view
@decorator_from_middleware(ExtendTokenResponse)
def view_function(request):
...
#with class view
class SimpleMiddlewareMixin:
    @decorator_from_middleware(ExtendTokenResponse) 
    def dispatch(*args, **kwargs):
        return super().dispatch(*args, **kwargs)

class MyClassBasedView(SimpleMiddlewareMixin, ListView):

Thành Lý
  • 86
  • 3
  • Hi, Method 2 looks promising but I'm not understanding how to apply it to what I have. – Dave Jun 23 '22 at 14:32