Opened 5 years ago

Closed 7 months ago

Last modified 4 months ago

#31405 closed New feature (fixed)

LoginRequiredAuthenticationMiddleware force all views to require authentication by default.

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

Description

Change History (14)

comment:1 by Carlton Gibson, 5 years ago

Triage Stage: UnreviewedAccepted

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

comment:2 by Mehmet İnce, 5 years ago

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

comment:3 by Carlton Gibson, 4 years ago

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.

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

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 by Nick Pope, 4 years ago

Has patch: set

Re-set the has patch flag removed by mistake.

comment:6 by Michael, 3 years ago

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

comment:7 by Hisham Mahmood, 11 months ago

Owner: changed from Mehmet INCE to Hisham Mahmood

comment:8 by Hisham Mahmood, 11 months ago

Needs documentation: unset

comment:9 by Sarah Boyce, 9 months ago

Patch needs improvement: set

comment:10 by Hisham Mahmood, 7 months ago

Patch needs improvement: unset

comment:11 by Sarah Boyce, 7 months ago

Triage Stage: AcceptedReady for checkin

comment:12 by Sarah Boyce <42296566+sarahboyce@…>, 7 months ago

Resolution: fixed
Status: assignedclosed

In c7fc9f20:

Fixed #31405 -- Added LoginRequiredMiddleware.

Co-authored-by: Adam Johnson <me@…>
Co-authored-by: Mehmet İnce <mehmet@…>
Co-authored-by: Sarah Boyce <42296566+sarahboyce@…>

comment:13 by Sarah Boyce <42296566+sarahboyce@…>, 4 months ago

In 49815f70:

Refs #31405 -- Improved LoginRequiredMiddleware documentation.

co-authored-by: Sarah Boyce <42296566+sarahboyce@…>

comment:14 by Sarah Boyce <42296566+sarahboyce@…>, 4 months ago

In 291fa5f:

[5.1.x] Refs #31405 -- Improved LoginRequiredMiddleware documentation.

co-authored-by: Sarah Boyce <42296566+sarahboyce@…>

Backport of 49815f70e4508ae21135f725da177fc2935de32c from main.

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