Repurposing default_token_generator, worked in 2.2 fails in 3.1

I’m in the process of upgrading an existing site that previously used an early version of Wagtail and Django 2.x, which I have now upgraded to use Django 3.1.

Previously, I repurposed functions from the PasswordResetTokenGenerator function to provide one-click email list unsubscribe functionality, which worked in Django 2.x but is failing in an odd way in Django 3.1. Since Django 3.x eliminated six I was prompted to revisit these functions and update them to work without six.

In apps that generate email to groups of users I append the body of any outgoing mail with an unsubscribe link containing a base64 encoded email address of the user and the password reset token made by the included Django password reset token function, like so…

from django.contrib.auth.tokens import default_token_generator

token = default_token_generator.make_token(self.user)
uid = urlsafe_base64_encode(force_bytes(self.user.email))
unsub_link = settings.BASE_URL + '/unsubscribe/' + uid + '/' + token + '/'

And then as a URL, I have…

 path('unsubscribe/<uidb64>/<token>/', views.unsubscribe, name='unsubscribe'),

And as the view…

from django.shortcuts import render
from django.contrib.auth import get_user_model
from django.utils.decorators import method_decorator
from django.utils.http import urlsafe_base64_decode
from django.core.exceptions import ValidationError
from django.contrib.auth.tokens import default_token_generator

UserModel = get_user_model()

def unsubscribe(request, uidb64, token):
    try:
        uid = urlsafe_base64_decode(uidb64).decode()
        user = UserModel.objects.get(email=uid)
    except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist, ValidationError):
        user = None
    if user is not None:
        token_generator = default_token_generator
        if token_generator.check_token(user, token):
            # If valid token, set is_subscribed = False and show confirmation.
            user.is_subscribed = False
            user.save()
            return render(request, 'account/unsubscribe_confirm.html')
        else:
            # Else, return invalid token page.
            return render(request, 'account/invalid_token.html')

This setup always returns an invalid token, but the funny thing is, if I step through the view function manually in the shell, check_token comes back true!

(anonymized values:)

>>> uid = 'me@gmail.com'
>>> user = UserModel.objects.get(email=uid)
>>> token_generator = default_token_generator
>>> token_generator.check_token(user, token='an08ff-atoken123456789020f3b65c9')
True

Should note that since posting this I’ve narrowed it down to uwsgi causing the issue somehow, but how is not apparent…

Is there any chance at all that the uwsgi instance is using either a different virtual environment with a different version of Django, or a different settings file with a different SECRET_KEY?
(The default_token_generator is set to PasswordResetTokenGenerator(), which uses the SECRET_KEY unless the class attribute secret is set when creating the class instance.)

Hey Ken, thanks for the reply.

I checked / double checked the uwsgi config against the env variable file in the folder that the bundled dev server environment was using, and can’t see any difference in the secret keys. I know I copied/pasted one to the other, so can’t see how that would produce different results, unless I ran into some sort of character encoding oddity between my laptop and the tty on the remote server?

FWIW, I did install Gunicorn and reproduce the server I had set up with it, and Gunicorn works fine (with the env variables that I was using with the dev server loaded), so I think this can be chalked up to some sort of bug with or config oddity of uwsgi that I am missing, and taken up with them.