Using middleware class for user authentication in Django project - python

One of my company's project is in Django and I was assigned to a task to 'add authentication to the top-level router' so we don't forget to add an authentication code to every view, such as:
if not request.user.is_authenticated:
return HttpResponseRedirect('/admin/login/?next=/admin/some-page/')
After doing a bit of research I found out that this can be accomplished by writing my own middleware. I was not familiar at all with the concept until doing my homework. So now I am trying to write a LoginRequiredMiddleware class that intercepts every request and sends the user to the login page if not authenticated and, after authentication, to the original page the user was trying to access.
This is the code I have so far.
middleware.py
from django.conf import settings
from django.http import HttpResponseRedirect
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import is_safe_url
import re
EXEMPT_URLS = [re.compile(settings.LOGIN_REDIRECT_URL.lstrip('/'))] # '/admin'
class LoginRequiredMiddleware(MiddlewareMixin):
def process_request(self, request):
assert hasattr(request, 'user'), "The Login Required Middleware"
if not request.user.is_authenticated:
path = request.path_info.lstrip('/')
if not any(m.match(path) for m in EXEMPT_URLS):
redirect_to = settings.LOGIN_REDIRECT_URL
if len(path) > 0 and is_safe_url(
url=request.path_info, allowed_hosts=request.get_host()):
redirect_to = f"{settings.LOGIN_REDIRECT_URL}/login?next={request.path_info}"
return HttpResponseRedirect(redirect_to)
I have already registered the middleware in the MIDDLEWARE list in settings.py and included both SessionMiddleware and AuthenticationMiddleware but I have not managed to get it to work. I can access a page that requires authentication in incognito mode, for example, without logging in.
I would like some tips on what am I doing wrong or which better path I should be following.

import re
from django.conf import settings
from django.shortcuts import redirect
from django.contrib.auth import logout
from django.utils.deprecation import MiddlewareMixin
EXEMPT_URLS = [re.compile(settings.LOGIN_URL.lstrip('/'))]
if hasattr(settings, 'LOGIN_EXEMPT_URLS'):
EXEMPT_URLS += [re.compile(url) for url in settings.LOGIN_EXEMPT_URLS]
# Other login exempt urls can be added in settings with 'LOGIN_EXEMPT_URLS'
class LoginRequiredMiddleware(MiddlewareMixin):
def __init__(self, get_response):
self.get_response = get_response
def process_request(self, request):
assert hasattr(request, 'user')
path = request.path_info.lstrip('/')
for endpoint in ["api/", "settings", "mfa", "reset"]:
if path.startswith(endpoint):
return self.get_response(request)
url_is_exempt = any(url.match(path) for url in EXEMPT_URLS)
if path == settings.LOGOUT_URL.lstrip('/'):
logout(request)
if request.user.is_authenticated and url_is_exempt:
# If the user is authenticated and the URL is in the exempt list
# redirect to the login page
return redirect(settings.LOGIN_REDIRECT_URL)
elif request.user.is_authenticated or url_is_exempt:
# Do nothing if the user is authenticated and the URL is not in the
# exempt list
return None
else:
# Trying to access any page as a non authenticated user
return redirect(f"{settings.LOGIN_URL}?next=/{path}")

Related

How do I test all of my app's urls in Django to make sure they require authentication?

I want to write a Django test that looks at all the urls in one particular app, and makes sure that hitting them without being authenticated redirects me to the login page.
Something like:
from django.test import TestCase
from django.urls import reverse
from my_app.urls import urlpatterns
class AuthTest(TestCase):
def _test_url(self, url):
response = self.client.get(url)
self.assertRedirects(response, expected_url=reverse("login_app:login")+ "?next=" + url)
def test_auth(self):
for url in urlpatterns.SOMETHING:
with self.subTest(url=url):
self._test_url(url)
Is there a way to get the actual list of urls in each urlpattern for my app?
And, related question: for those URLs that require a pk, is there a way to automatically use pk=1? My fixtures will make sure that pk=1 exists for all those urls.
As our applications needs authentication by default, we used a middleware to make sure the user is login except in case of some exempted paths like API Endpoints that are secure by our means
The middleware goes like this
from django.http import HttpResponseRedirect, HttpResponse
from django.conf import settings
class UserMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
state = request.user.is_authenticated
path = request.META["PATH_INFO"]
if not state and not path in settings.EXEMPTED_PATHS:
url = settings.LOGIN_URL
if not "next" in path:
if "?" in URL:
url += '&'
else: url += "?"
url += "next=%s" % (
settings.BASE_URL + path).replace("//", "/")
if len(request.GET) > 0:
if "?" in URL:
url += "?"
else: url += "&"
url += "%s" % request.GET.urlencode().replace("//", "/")
return HttpResponseRedirect(url)
return self.get_response(request)

How to get Django LoginRequiredMiddleware to return a 401_UNAUTHORIZED code instead of 302_FOUND

I am writing a custom basic LoginRequiredMiddleware for my Django project and so far everything works as expected. I am using process_request because I thought I wanted the request to be checked for authentication before the view is processed.
However, this middleware fails at a test that other developers have written:
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
AssertionError: 302 != 401
This happens because the view is discovered even if the user is not authenticated when it should actually return a status.HTTP_401_UNAUTHORIZED. How can I rewrite my middleware to comply with the test and return the correct status code?
middleware.py
import re
from django.conf import settings
from django.shortcuts import redirect
from django.contrib.auth import logout
from django.utils.deprecation import MiddlewareMixin
EXEMPT_URLS = [re.compile(settings.LOGIN_URL.lstrip('/'))]
if hasattr(settings, 'LOGIN_EXEMPT_URLS'):
EXEMPT_URLS += [re.compile(url) for url in settings.LOGIN_EXEMPT_URLS]
class LoginRequiredMiddleware(MiddlewareMixin):
def process_request(self, request):
assert hasattr(request, 'user')
path = request.path_info.lstrip('/')
url_is_exempt = any(url.match(path) for url in EXEMPT_URLS)
if path == settings.LOGOUT_URL.lstrip('/'):
logout(request)
if request.user.is_authenticated and url_is_exempt:
return redirect(settings.LOGIN_REDIRECT_URL)
elif request.user.is_authenticated or url_is_exempt:
return None
else:
return redirect(f"{settings.LOGIN_URL}?next=/{path}")
Something like:
class Redirect401Response(HttpResponseRedirect):
status_code = 401
def process_request(request):
...
return Redirect401Response(resolve_url(f"{settings.LOGIN_URL}?next=/{path}"))
?
The 302 stems from the return redirect(f'{settings.LOGIN_URL}?next=/{path}') that you're doing to the login URL in the last step. A redirect will (and should, I'm disagreeing here with the answer which suggests redirecting and sending a 401) always yield a status code 302. You probably want to do a return HttpResponse('Unauthorized', status=401) but then you loose the convenience of forwarding your users to the login URL.

Custom middleware returning redirect loop

I have referred to the code shared on this website http://onecreativeblog.com/post/59051248/django-login-required-middleware
to make a login required middleware. The problem is it is running into redirect loop.
import re
from django.conf import settings
from django.urls import reverse
from django.shortcuts import redirect
from django.contrib.auth import logout
EXEMPT_URLS = [re.compile(settings.LOGIN_URL.lstrip('/'))]
if hasattr(settings, 'LOGIN_EXEMPT_URLS'):
EXEMPT_URLS += [re.compile(url) for url in
settings.LOGIN_EXEMPT_URLS]
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
assert hasattr(request, 'user')
path = request.path_info.lstrip('/')
if not request.user.is_authenticated():
if not any(url.match(path) for url in EXEMPT_URLS):
return redirect(settings.LOGIN_URL)
I have looked into stackoverflow and tried almost every solution but in vain.
Settings url
LOGIN_REDIRECT_URL='/blog/index/'
LOGIN_EXEMPT_URLS= (
r'^blog/signup/$',
r'^blog/logout/$',
)
Any help would be appreciated! Thanks in advance.

login_required not working django even after cache clear

I want the user not to navigate back once he is logged out. I used login_required(login_url = "/login/") before my definition of logout view in my views.py. I am still able to navigate back after logging out. How can I ask the user to log in again? Also, If I type URL for my home page (say localhost:8000/home) directly in the browser, I am able to access the page without login. How can I correct this issue?
I have cleared browser cache too and still, no use.
my views.py:
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.contrib import auth
from django.views.decorators import csrf
from django.contrib.auth.decorators import login_required
def login(request):
c={}
# c.update(csrf(request))
return render(request,'login.html',c)
def auth_view(request):
username = request.POST.get('username','')
password = request.POST.get('password','')
user = auth.authenticate(username=username, password=password)
if user is not None:
auth.login(request, user)
return HttpResponseRedirect('/home')
#login_required(login_url='/login/')
def logout(request):
request.session.flush()
auth.logout(request)
return render(request,'logout.html')
If you want to protect your home page or other pages with login, you should decorate them with login_required decorator. For example,
#login_required(login_url='/login/')
def home(request):
# Your code goes here

Django, redirect all non-authenticated users to landing page

I have a django website with many urls and views. Now I have asked to redirect all non-authenticated users to a certain landing page. So, all views must check if user.is_authenticated() and return to a new set of landing pages.
Can it be done in a pretty way, instead of messing with my views.py/urls.py that much?
There is a simpler way to do this, just add the "login_url" parameter to #login_required and if the user is not login he will be redirected to the login page. You can find it here
from django.contrib.auth.decorators import login_required
#login_required(login_url='/accounts/login/')
def my_view(request):
...
You can use Middleware.
Something like this will check user auth every request:
class AuthRequiredMiddleware(object):
def process_request(self, request):
if not request.user.is_authenticated():
return HttpResponseRedirect(reverse('landing_page')) # or http response
return None
Docs: process_request
Also, don't forget to enable it in settings.py
MIDDLEWARE_CLASSES = (
...
'path.to.your.AuthRequiredMiddleware',
)
see the docs for login required decorator
from django.contrib.auth.decorators import login_required
#login_required
def my_view(request):
...
another option is to add it to your urls.py patterns, see this answer
urlpatterns = patterns('',
(r'^foo/$', login_required(direct_to_template), {'template': 'foo_index.html'}),
)
As of Django 1.10, the custom middleware classes must implement the new style syntax. You can use the following class to verify that the user is logged in while trying to access any views.
from django.shortcuts import HttpResponseRedirect
class AuthRequiredMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
if not request.user.is_authenticated: # in Django > 3 this is a boolean
return HttpResponseRedirect('login')
# Code to be executed for each request/response after
# the view is called.
return response
You can avoid specifying login_url by setting LOGIN_URL.
Therefore, in settings.py add:
LOGIN_URL = '<some_url>'
And in your views.py annotate relevant functions with only #login_required:
#login_required
def some_view_function(request):
If you need to redirect within a view function, you can do so with:
return redirect_to_login(request.get_full_path())
This can be done with middleware.
I've found a really nifty djangosnippet that does exactly what you are asking for. You can find it here, and it looks like:
from django.http import HttpResponseRedirect
from django.conf import settings
from re import compile
EXEMPT_URLS = [compile(settings.LOGIN_URL.lstrip('/'))]
if hasattr(settings, 'LOGIN_EXEMPT_URLS'):
EXEMPT_URLS += [compile(expr) for expr in settings.LOGIN_EXEMPT_URLS]
class LoginRequiredMiddleware:
"""
Middleware that requires a user to be authenticated to view any page other
than LOGIN_URL. Exemptions to this requirement can optionally be specified
in settings via a list of regular expressions in LOGIN_EXEMPT_URLS (which
you can copy from your urls.py).
Requires authentication middleware and template context processors to be
loaded. You'll get an error if they aren't.
"""
def process_request(self, request):
assert hasattr(request, 'user'), "The Login Required middleware\
requires authentication middleware to be installed. Edit your\
MIDDLEWARE_CLASSES setting to insert\
'django.contrib.auth.middlware.AuthenticationMiddleware'. If that doesn't\
work, ensure your TEMPLATE_CONTEXT_PROCESSORS setting includes\
'django.core.context_processors.auth'."
if not request.user.is_authenticated():
path = request.path_info.lstrip('/')
if not any(m.match(path) for m in EXEMPT_URLS):
return HttpResponseRedirect(settings.LOGIN_URL)
All you have to do is to save the file as middleware.py and include the class in you're settings.py, i.e.
MIDDLEWARE_CLASSES += ('projectname.common.middleware.RequireLoginMiddleware',)
You can also define a LOGIN_URL in settings.py, so that you'll be redirected to your custom login page. The default LOGIN_URL is '/accounts/login/'.
Maybe too late but in django 1.9+ it's too easy.
Django introduced Login Required mixin for generic classes and this a great example
here by William S. Vincent
simply in your view add LoginRequiredMixin as parent class
from django.contrib.auth.mixins import LoginRequiredMixin
class BlogUpdateView(LoginRequiredMixin, UpdateView):
model = Post
template_name = 'post_edit.html'
fields = ['title', 'body']
Also you can use login_required decorator for method request
from django.contrib.auth.decorators import login_required
#login_required(login_url='/login/')
def home(request):
return render(request, "home.html")
It's showing like this: http://127.0.0.1:1235/login/?next=/home/
I'm using class based views and I couldn't get any of these solutions to work for what I needed so I'll post my working solution here.
class ManagementPageView(TemplateView):
# Make a dispatch method to handle authentication
def dispatch(self, *args, **kwargs):
# Check if user is authenticated
if not self.request.user.is_authenticated:
# Redirect them to the home page if not
return redirect('home')
# Render the template if they are
return render(self.request, 'management.html')

Categories

Resources