pythondjango

Best way to make Django's login_required the default


I'm working on a large Django app, the vast majority of which requires a login to access. This means that all throughout our app we've sprinkled:

@login_required
def view(...):

That's fine, and it works great as long as we remember to add it everywhere! Sadly sometimes we forget, and the failure often isn't terribly evident. If the only link to a view is on a @login_required page then you're not likely to notice that you can actually reach that view without logging in. But the bad guys might notice, which is a problem.

My idea was to reverse the system. Instead of having to type @login_required everywhere, instead I'd have something like:

@public
def public_view(...):

Just for the public stuff. I tried to implement this with some middleware and I couldn't seem to get it to work. Everything I tried interacted badly with other middleware we're using, I think. Next up I tried writing something to traverse the URL patterns to check that everything that's not @public was marked @login_required - at least then we'd get a quick error if we forgot something. But then I couldn't figure out how to tell if @login_required had been applied to a view...

So, what's the right way to do this? Thanks for the help!


Solution

  • Middleware may be your best bet. I've used this piece of code in the past, modified from a snippet found elsewhere:

    import re
    
    from django.conf import settings
    from django.contrib.auth.decorators import login_required
    
    
    class RequireLoginMiddleware(object):
        """
        Middleware component that wraps the login_required decorator around
        matching URL patterns. To use, add the class to MIDDLEWARE_CLASSES and
        define LOGIN_REQUIRED_URLS and LOGIN_REQUIRED_URLS_EXCEPTIONS in your
        settings.py. For example:
        ------
        LOGIN_REQUIRED_URLS = (
            r'/topsecret/(.*)$',
        )
        LOGIN_REQUIRED_URLS_EXCEPTIONS = (
            r'/topsecret/login(.*)$',
            r'/topsecret/logout(.*)$',
        )
        ------
        LOGIN_REQUIRED_URLS is where you define URL patterns; each pattern must
        be a valid regex.
    
        LOGIN_REQUIRED_URLS_EXCEPTIONS is, conversely, where you explicitly
        define any exceptions (like login and logout URLs).
        """
        def __init__(self):
            self.required = tuple(re.compile(url) for url in settings.LOGIN_REQUIRED_URLS)
            self.exceptions = tuple(re.compile(url) for url in settings.LOGIN_REQUIRED_URLS_EXCEPTIONS)
    
        def process_view(self, request, view_func, view_args, view_kwargs):
            # No need to process URLs if user already logged in
            if request.user.is_authenticated():
                return None
    
            # An exception match should immediately return None
            for url in self.exceptions:
                if url.match(request.path):
                    return None
    
            # Requests matching a restricted URL pattern are returned
            # wrapped with the login_required decorator
            for url in self.required:
                if url.match(request.path):
                    return login_required(view_func)(request, *view_args, **view_kwargs)
    
            # Explicitly return None for all non-matching requests
            return None
    

    Then in settings.py, list the base URLs you want to protect:

    LOGIN_REQUIRED_URLS = (
        r'/private_stuff/(.*)$',
        r'/login_required/(.*)$',
    )
    

    As long as your site follows URL conventions for the pages requiring authentication, this model will work. If this isn't a one-to-one fit, you may choose to modify the middleware to suit your circumstances more closely.

    What I like about this approach - besides removing the necessity of littering the codebase with @login_required decorators - is that if the authentication scheme changes, you have one place to go to make global changes.