Opened 2 years ago

Last modified 4 months ago

#31405 assigned New feature

LoginRequiredAuthenticationMiddleware force all views to require authentication by default.

Reported by: Mehmet İnce Owned by: Mehmet INCE
Component: contrib.auth Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Change History (6)

comment:1 Changed 2 years ago by Carlton Gibson

Triage Stage: UnreviewedAccepted

I'll Accept based on the mailing list discussion. Thanks.

comment:2 Changed 2 years ago by Mehmet İnce

Has patch: set
Owner: changed from nobody to Mehmet İnce
Status: newassigned

comment:3 Changed 2 years ago by Carlton Gibson

Needs documentation: set
Needs tests: set
Owner: changed from Mehmet İnce to Mehmet INCE
Patch needs improvement: set

Thanks Mehmet. Comments on PR — Please uncheck flags when address to put it back in the review queue.

comment:4 in reply to:  3 Changed 2 years ago by Mehmet INCE

Has patch: unset
Needs tests: unset
Patch needs improvement: unset

Replying to Carlton Gibson:

Thanks Mehmet. Comments on PR — Please uncheck flags when address to put it back in the review queue.

Thansk for the review Carlton. I believe that I solved the issues you pointed.

There were nice people from the mailing list who are willing to help out with docs. Once we are finished everything, I'll ping them for the docs :)

comment:5 Changed 21 months ago by Nick Pope

Has patch: set

Re-set the has patch flag removed by mistake.

comment:6 Changed 4 months ago by Michael

I am very interested in this new feature. Will it have a way to mark function and class based views as no login requied?

Probably too late but heres some code from my solution:

A decorator to mark a view/function as no longer required:

from functools import wraps


def login_not_required(obj):
    """Adds the attrbiute login_not_required = True to the object (func/class).

    Use it as follows:
        @login_not_required
        class FooView(generic.View):
            ...

        @login_not_required
        def bar_view(request):
            ...
    """

    @wraps(obj)
    def decorator():
        obj.login_not_required = True  # For general pages
        obj.permission_classes = []  # For REST framework
        return obj

    return decorator()

Middleware:

# settings.py
NONE_AUTH_ACCOUNT_PATHS = [
   ....   
    '/accounts/password_reset/',
    '/accounts/reset/',
]

# middleware.py
class RequireLoginCheck:
    """Middleware to require authentication on all views by default, except when allowed.

    URLS can be opened by adding them to NONE_AUTH_ACCOUNT_PATHS, or by adding
    the @login_not_required decorator.

    Must appear below the sessions middleware because the sessions middleware
    adds the user to the request, which is used by this middleware.
    """

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

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

    def _is_none_auth_path(self, path):
        for none_auth_path in NONE_AUTH_ACCOUNT_PATHS:
            if path.startswith(none_auth_path):
                return True
        return False

    def _is_login_not_required(self, view_func):
        with suppress(AttributeError):
            # If a class with the @login_not_required decorator, will return True
            return view_func.view_class.login_not_required
        with suppress(AttributeError):
            # If a function with the @login_not_required decorator, will return True
            return view_func.login_not_required
        return False

    def _is_open_rest_view(self, view_func):
        try:
            klass = view_func.view_class
        except AttributeError:
            return False
        if not issubclass(view_func.view_class, APIView):
            return False
        else:
            auth_classes = getattr(klass, 'authentication_classes', None)
            perm_classes = getattr(klass, 'permission_classes', None)
            # if auth_classes and perm_classes are empty list/tuples, then don't require login checks
            no_login_required = (
                auth_classes is not None
                and not auth_classes
                and perm_classes is not None
                and not perm_classes
            )
            return no_login_required

    def log_unauthorised_request(self, request, view_func, view_args, view_kwargs):
        get_response = lambda: HTTP_NO_RESPONSE
        reason = CsrfViewMiddleware(get_response).process_view(request, None, (), {})
        s = ["base.auth.middleware.RequireLoginCheck"]
        s.append(f"User: {request.user}")
        s.append(f"Method: {request.method}")
        s.append(f"URL: {request.path}")
        s.append(f"IP: {get_ip(request)}")
        s.append(f"Reason: {reason}")
        s.append(f"Open URL (is_login_not_required): {self._is_login_not_required(view_func)}")
        s.append(f"is_none_auth_path: {self._is_none_auth_path(request.path)}")
        s.append(f"HEADERS: {request.headers}")
        s.append(f"GET: {request.GET}")
        s.append(f"POST: {request.POST}")
        if LOGGING:
            log_info(', '.join(s))
        if settings.DEBUG and not request.path.startswith('static'):
            print(', '.join(s))

    def process_view(self, request, view_func, view_args, view_kwargs):
        """https://docs.djangoproject.com/en/stable/topics/http/middleware/#other-middleware-hooks"""
        if not (
            request.user.is_authenticated
            or self._is_login_not_required(view_func)
            or self._is_open_rest_view(view_func)
            or self._is_none_auth_path(request.path)
        ):
            self.log_unauthorised_request(request, view_func, view_args, view_kwargs)
            if settings.LOGIN_URL != request.path:
                # if next URL after login is the same login URL, then cyclic loop
                return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
            else:
                return redirect('%s?next=%s' % (settings.LOGIN_URL, '/'))
        return None

Note: See TracTickets for help on using tickets.
Back to Top