Search code examples
pythondjangodjango-sessions

How we patched Django to keep users logged in between sessions


We had a problem with a website which uses Django. Each time we upgrade Django, if a user is logged in with two or more different browsers, and then they login again from one browser - they are automatically logged out from all other sessions (browsers). Since we upgraded Django to new major versions about 5 times in the last year, this caused us a headache. We don't want to force users to have to login again and again between sessions. How can we solve this problem?


Solution

  • We checked and found out that this problem is caused due to a change in PBKDF2PasswordHasher.iterations between versions of Django. Each time we upgrade Django to a new major version (such as from 3.0 to 3.1), PBKDF2PasswordHasher.iterations changes. This causes the user's hashed password to be calculated again the next time the user logs in, which forces the user to remain logged out in all other sessions. I even created a ticket with Django's tracking system.

    There are two options to fix this issue. First, we can patch class PBKDF2PasswordHasher to keep the number of iterations constant, and also update def must_update:

    from django.contrib.auth.hashers import PBKDF2PasswordHasher
    
    
    def patch():
        def must_update(self, encoded):
            # Update the stored password only if the iterations diff is at least 250,000.
            algorithm, iterations, salt, hash = encoded.split('$', 3)
            iterations_diff = abs(self.iterations - int(iterations))
            return ((int(iterations) != self.iterations) and (iterations_diff >= 250000))
    
        PBKDF2PasswordHasher.iterations = 180000  # Django 3.0.x
        PBKDF2PasswordHasher.must_update = must_update
    

    And then in our base AppConfig class:

    class SpeedyCoreBaseConfig(AppConfig):
        name = 'speedy.core.base'
        verbose_name = _("Speedy Core Base App")
        label = 'base'
    
        def ready(self):
            locale_patches.patch()  # Another patch
            session_patches.patch()  # This patch
    

    Or, you can inherit a new class from PBKDF2PasswordHasher, change iterations and def must_update, and use your new class in the settings (PASSWORD_HASHERS). We used the first option, although it might be better to use the second option (inherit a new class) from a software engineering perspective. They both work.