Skip to content Skip to sidebar Skip to footer

Is It Possible To Implement A "change Password At Next Logon" Type Feature In The Django Admin?

I want to be able to set an option in the user's settings that forces them to change their password upon the next login to the admin interface. Is this possible? How would it go ab

Solution 1:

I'm actually in the process of doing this myself. You need three components: a user profile (if not already in use on your site), a middleware component, and a pre_save signal.

My code for this is in an app named 'accounts'.

# myproject/accounts/models.pyfrom django.db import models
from django.db.models import signals
from django.contrib.auth.models import User

classUserProfile(models.Model):
    user = models.ForeignKey(User, unique=True)
    force_password_change = models.BooleanField(default=False)

defcreate_user_profile_signal(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

defpassword_change_signal(sender, instance, **kwargs):
    try:
        user = User.objects.get(username=instance.username)
        ifnot user.password == instance.password:
          profile = user.get_profile()
          profile.force_password_change = False
          profile.save()
    except User.DoesNotExist:
        pass

signals.pre_save.connect(password_change_signal, sender=User, dispatch_uid='accounts.models')

signals.post_save.connect(create_user_profile_signal, sender=User, dispatch_uid='accounts.models')

First, we create a UserProfile with a foreign key to User. The force_password_change boolean will, as its name describes, be set to true for a user whenever you want to force them to change their password. You could do anything here though. In my organization, we also chose to implement a mandatory change every 90 days, so I also have a DateTimeField that stores the last time a user changed their password. You then set that in the pre_save signal, password_changed_signal.

Second, we have the create_user_profile_signal. This is mostly added just for completeness. If you're just now adding user profiles into your project, you'll need a post_save signal that will create a UserProfile every time a User is created. This accomplishes that task.

Third, we have the password_changed_signal. This is a pre_save signal because at this point in the process the actual row in the User table hasn't be updated. Therefore, we can access both the previous password and the new password about to be saved. If the two don't match, that means the user has changed their password, and we can then reset the force_password_change boolean. This would be the point, also where you would take care of any other things you've added such as setting the DateTimeField previously mentioned.

The last two lines attach the two functions to their appropriate signals.

If you haven't already, you will also need to add the following line to your project's settings.py (changing the app label and model name to match your setup):

AUTH_PROFILE_MODULE = 'accounts.UserProfile'

That covers the basics. Now we need a middleware component to check the status of our force_password_change flag (and any other necessary checks).

# myproject/accounts/middleware.pyfrom django.http import HttpResponseRedirect

import re

classPasswordChangeMiddleware:
    defprocess_request(self, request):
        if request.user.is_authenticated() and \
            re.match(r'^/admin/?', request.path) and \
            not re.match(r'^/admin/password_change/?', request.path):

            profile = request.user.get_profile()
            if profile.force_password_change:
                return HttpResponseRedirect('/admin/password_change/')

This very simple middleware hooks into the process_request stage of the page loading process. It checks that 1) the user has already logged in, 2) they are trying to access some page in the admin, and 3) the page they are accessing is not the password change page itself (otherwise, you'd get an infinite loop of redirects). If all of these are true and the force_password_change flag has been set to True, then the user is redirected to the password change page. They will not be able to navigate anywhere else until they change their password (firing the pre_save signal discussed previously).

Finally, you just need to add this middleware to your project's settings.py (again, changing the import path as necessary):

MIDDLEWARE_CLASSES = (
    # Other middleware here'myproject.accounts.middleware.PasswordChangeMiddleware',
)

Solution 2:

I have used Chris Pratt's solution, with a little change: instead of using a middleware, that'd be executed for every page with the consequent resource use, I figured I'd just intercept the login view.

In my urls.py I have added this to my urlpatterns:

url(r'^accounts/login/$', 'userbase.views.force_pwd_login'),

then I added the following to userbase.views:

defforce_pwd_login(request, *args, **kwargs):
    response = auth_views.login(request, *args, **kwargs)
    if response.status_code == 302:
        #We have a usertry:
            if request.user.get_profile().force_password_change:
                return redirect('django.contrib.auth.views.password_change')
        except AttributeError: #No profile?passreturn response

It seems to work flawlessly on Django 1.2, but I have no reason to believe 1.3+ should have problems with it.

Solution 3:

This is the middleware I use with Django 1.11 :

# myproject/accounts/middleware.pyfrom django.http import HttpResponseRedirect
from django.urls import reverse


classPasswordChangeMiddleware:
    def__init__(self, get_response):
        self.get_response = get_response

    def__call__(self, request):
        response = self.get_response(request)
        next = reverse('client:password-update')
        if request.user.is_authenticated() and request.path != next:
            if request.user.account.force_password_change:
                return HttpResponseRedirect(next)

        return response

Still adding it to the settings middleware list :

MIDDLEWARE_CLASSES = (
    # Other middleware here'myproject.accounts.middleware.PasswordChangeMiddleware',
)

Solution 4:

I spent 2 days on this issue recently, and a new solution came out. Hopefully it's useful.

Just as above said, a new user model created.

newuser/models.py

classUsers(AbstractUser):
    default_pwd_updated = models.NullBooleanField(default=None, editable=False)
    pwd_update_time = models.DateTimeField(editable=False, null=True, default=None)  # reserved column to support further interval password (such as 60 days) update policydefset_password(self, raw_password):
        if self.default_pwd_updated isNone:
            self.default_pwd_updated = Falseelifnot self.default_pwd_updated:
            self.default_pwd_updated = True
            self.pwd_update_time = timezone.now()
        else:
            self.pwd_update_time = timezone.now()
        super().set_password(raw_password)

Set this model as the AUTH_USER_MODEL.

[project]/settings.py

AUTH_USER_MODEL = 'newuser.Users'

Now you just need to customize LoginView and some methods in AdminSite.

[project]/admin.py

from django.contrib.admin import AdminSite
from django.contrib.auth.views import LoginView
from django.utils.translation import gettext as _, gettext_lazy
from django.urls import reverse
from django.views.decorators.cache import never_cache
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect


classNewLoginView(LoginView):
    defget_redirect_url(self):
        if self.request.method == "POST"and self.request.user.get_username()\
                andnot self.request.user.default_pwd_updated:
            redirect_to = reverse("admin:password_change")
        else:
            redirect_to = self.request.POST.get(
                self.redirect_field_name,
                self.request.GET.get(self.redirect_field_name, '')
            )
        return redirect_to


classNewAdminSite(AdminSite):
    site_header = site_title = gettext_lazy("Customized Admin Site")

    def__init__(self, name="admin"):
        super().__init__(name)

    @never_cachedeflogin(self, request, extra_context=None):
        """
        Display the login form for the given HttpRequest.
        """if request.method == 'GET'and self.has_permission(request):
            # Already logged-in, redirect to admin indexif request.user.get_username() andnot request.user.default_pwd_updated:
                # default password not changed, force to password_change view
                path = reverse('admin:password_change', current_app=self.name)
            else:
                path = reverse('admin:index', current_app=self.name)
            return HttpResponseRedirect(path)

        from django.contrib.auth.views import LoginView
        from django.contrib.admin.forms import AdminAuthenticationForm
        context = {
            **self.each_context(request),
            'title': _('Log in'),
            'app_path': request.get_full_path(),
            'username': request.user.get_username(),
        }
        if (REDIRECT_FIELD_NAME notin request.GET and
                REDIRECT_FIELD_NAME notin request.POST):
            context[REDIRECT_FIELD_NAME] = reverse('admin:index', current_app=self.name)
        context.update(extra_context or {})

        defaults = {
            'extra_context': context,
            'authentication_form': self.login_form or AdminAuthenticationForm,
            'template_name': self.login_template or'admin/login.html',
        }
        request.current_app = self.name
        return NewLoginView.as_view(**defaults)(request) # use NewLoginView    @never_cachedefindex(self, request, extra_context=None):
        if request.user.get_username() andnot request.user.default_pwd_updated:
            # if default password not updated, force to password_change page
            context = self.each_context(request)
            context.update(extra_context or {})
            return self.password_change(request, context)
        returnsuper().index(request, extra_context)


admin_site = NewAdminSite(name="admin")

NOTE: if you intend to use custom template for changing default password, you could override each_context method and then determine which template should be used up to the flag force_pwd_change.

[project]/admin.py

defusing_default_password(self, request):
    if self.has_permission(request) and request.user.get_username() andnot request.user.default_pwd_updated:
            returnTruereturnFalsedefeach_context(self, request):
    context = super().each_context(request)
    context["force_pwd_change"] = self.using_default_password(request)
    return context

Solution 5:

From a thread on the Django Users mailing list:

This isn't ideal, but it should work (or prompt someone to propose something better).

Add a one-to-one table for the user, with a field containing the initial password (encrypted, of course, so it looks like the password in the auth_user table).

When the user logs in, have the login page check to see if the passwords match. If they do, redirect to the password change page instead of the normal redirect page.

Post a Comment for "Is It Possible To Implement A "change Password At Next Logon" Type Feature In The Django Admin?"