web-dev-qa-db-fra.com

Django REST-Auth Password Reset

Je suis complètement confus par le middleware Django disponible:

Je veux simplement faire fonctionner la fonctionnalité de réinitialisation de mot de passe (et plus tard de changement de mot de passe), en utilisant Django avec rest_auth Sur le backend et Vue sur le frontend.

Étape 1: demande de réinitialisation du lien par courrier

Vues

Jusqu'à présent, j'ai fait un CustomPasswordResetView:

# project/accounts/views.py
from rest_auth.views import PasswordResetView

class CustomPasswordResetView(PasswordResetView):
pass

Sérialiseurs

et un CustomPasswordResetSerializer:

# project/accounts/serializers.py
from rest_auth.serializers import PasswordResetSerializer

class CustomPasswordResetSerializer(PasswordResetSerializer):
    email = serializers.EmailField()
    password_reset_form_class = ResetPasswordForm

    def validate_email(self, value):
        # Create PasswordResetForm with the serializer
        self.reset_form = self.password_reset_form_class(data=self.initial_data)
        if not self.reset_form.is_valid():
            raise serializers.ValidationError(self.reset_form.errors)

        ###### FILTER YOUR USER MODEL ######
        if not get_user_model().objects.filter(email=value).exists():
            raise serializers.ValidationError(_('Invalid e-mail address'))

        return value

    def save(self):
        request = self.context.get('request')
        # Set some values to trigger the send_email method.
        opts = {
            'use_https': request.is_secure(),
            'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
            'request': request,
        }
        opts.update(self.get_email_options())
        self.reset_form.save(**opts)

Settings.py

Dans le settings.py J'ai ces champs, qui me semblent pertinents pour mon problème:

# project/vuedj/settings.py
REST_AUTH_SERIALIZERS = {
    "USER_DETAILS_SERIALIZER": "accounts.serializers.CustomUserDetailsSerializer",
    "LOGIN_SERIALIZER": "accounts.serializers.CustomUserLoginSerializer",
    "PASSWORD_RESET_SERIALIZER": "accounts.serializers.CustomPasswordResetSerializer"
}

(Le settings.py Complet est joint en bas)

Modèles d'URL

Mes URL détectent déjà ma demande d'API afin d'envoyer l'e-mail de réinitialisation du mot de passe:

# project/vuedj/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('api.urls')),
    path('accounts/', include('allauth.urls')),
    path('', api_views.index, name='home')
]
# project/api/urls.py
urlpatterns = [
    path('auth/', include('accounts.urls')),
    # other paths...
]
# project/accounts/urls.py
urlpatterns = [
    path('', acc_views.UserListView.as_view(), name='user-list'),
    path('login/', acc_views.UserLoginView.as_view(), name='login'),
    path('logout/', acc_views.UserLogoutView.as_view(),  name='logout'),
    path('register/', acc_views.CustomRegisterView.as_view(),  name='register'),
    path('reset-password/', acc_views.CustomPasswordResetView.as_view(), name='reset-password'),
    path('reset-password-confirm/', acc_views.CustomPasswordResetConfirmView.as_view(), name='reset-password-confirm'),
    path('<int:pk>/', acc_views.UserDetailView.as_view(), name='user-detail')
]

Courriel avec PW-Reset Token Generator

La vue CustomPasswordReset va éventuellement générer un email Nice avec un lien Nice pw-reset. Le lien est valide, en cliquant dessus, je peux parfaitement réinitialiser le mot de passe à travers les modèles allauth.

Ce code est utilisé par rest-auth (indirectement) pour générer le jeton de réinitialisation:

# project/.venv/Lib/site-packages/allauth/account/forms.py
def save(self, request, **kwargs):
    current_site = get_current_site(request)
    email = self.cleaned_data["email"]
    token_generator = kwargs.get("token_generator",
                                 default_token_generator)

    for user in self.users:

        temp_key = token_generator.make_token(user)

        # save it to the password reset model
        # password_reset = PasswordReset(user=user, temp_key=temp_key)
        # password_reset.save()

        # send the password reset email
        path = reverse("account_reset_password_from_key",
                       kwargs=dict(uidb36=user_pk_to_url_str(user),
                                   key=temp_key))
        url = build_absolute_uri(
            request, path)

        context = {"current_site": current_site,
                   "user": user,
                   "password_reset_url": url,
                   "request": request}

        if app_settings.AUTHENTICATION_METHOD \
                != AuthenticationMethod.EMAIL:
            context['username'] = user_username(user)
        get_adapter(request).send_mail(
            'account/email/password_reset_key',
            email,
            context)
    return self.cleaned_data["email"]

Ce PasswordResetTokenGenerator est utilisé dans le code ci-dessus:

# project/.venv/Lib/site-packages/Django/contrib/auth/tokens.py
class PasswordResetTokenGenerator:
        """
        Strategy object used to generate and check tokens for the password
        reset mechanism.
        """
        key_salt = "Django.contrib.auth.tokens.PasswordResetTokenGenerator"
        secret = settings.SECRET_KEY

        def make_token(self, user):
                """
                Return a token that can be used once to do a password reset
                for the given user.
                """
                return self._make_token_with_timestamp(user, self._num_days(self._today()))

        def check_token(self, user, token):
                """
                Check that a password reset token is correct for a given user.
                """
                if not (user and token):
                        return False
                # Parse the token
                try:
                        ts_b36, hash = token.split("-")
                except ValueError:
                        return False

                try:
                        ts = base36_to_int(ts_b36)
                except ValueError:
                        return False

                # Check that the timestamp/uid has not been tampered with
                if not constant_time_compare(self._make_token_with_timestamp(user, ts), token):
                        return False

                # Check the timestamp is within limit. Timestamps are rounded to
                # midnight (server time) providing a resolution of only 1 day. If a
                # link is generated 5 minutes before midnight and used 6 minutes later,
                # that counts as 1 day. Therefore, PASSWORD_RESET_TIMEOUT_DAYS = 1 means
                # "at least 1 day, could be up to 2."
                if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS:
                        return False

                return True

Les classes ci-dessus seront appelées par le rest_authPasswordResetView:

# project/.venv/Lib/site-packages/rest_auth/views.py
class PasswordResetView(GenericAPIView):
        """
        Calls Django Auth PasswordResetForm save method.

        Accepts the following POST parameters: email
        Returns the success/fail message.
        """
        serializer_class = PasswordResetSerializer
        permission_classes = (AllowAny,)

        def post(self, request, *args, **kwargs):
                # Create a serializer with request.data
                serializer = self.get_serializer(data=request.data)
                serializer.is_valid(raise_exception=True)

                serializer.save() # <----- Code from above (TokenGenerator) will be called inside this .save() method
                # Return the success message with OK HTTP status
                return Response(
                        {"detail": _("Password reset e-mail has been sent.")},
                        status=status.HTTP_200_OK
                )

Comme vous pouvez le voir, le Tokengenerator retournera un uidb36 Avec le jeton. Il suppose également un uidb36 Lorsque l'utilisateur confirmera la réinitialisation du mot de passe. Un jeton généré (par exemple le lien complet dans le courrier généré) ressemblerait à ceci:

http://localhost:8000/accounts/password/reset/key/16-52h-42b222e6dc30690b2e91/

16 est l'ID utilisateur dans la base 36 (uidb36), Je ne sais pas encore ce que 52h Signifie, mais je suppose que la troisième partie du jeton est le jeton lui-même (42b222e6dc30690b2e91)

Étape 2: envoyer le jeton au backend (alias "Lien de clics utilisateur")

Je suis coincé ici. Les API-Endpoints des Rest-Auth-Framework disent:

/ rest-auth/mot de passe/reset/confirm// (POST)
uid
token
new_password1
new_password2

Et quand j'envoie un objet par exemple:

{
    uid: '16', // TODO maybe I have to convert it to base10...
    token: '42b222e6dc30690b2e91',
    new_password1: 'test123A$',
    new_password2: 'test123A$'
}

via mon api à http://localhost:8000/api/v1/auth/reset-password/ avec l'objet ci-dessus dans le corps d'une demande de publication axios-, ma CustomPasswordResetConfirmView est déclenchée comme prévu, qui n'est aussi qu'une sous-classe de PasswordResetConfirmView from rest_auth, donc ce code est exécuté:

# project/.venv/Lib/site-packages/rest_auth/views.py
class PasswordResetConfirmView(GenericAPIView):
        """
        Password reset e-mail link is confirmed, therefore
        this resets the user's password.

        Accepts the following POST parameters: token, uid,
                new_password1, new_password2
        Returns the success/fail message.
        """
        serializer_class = PasswordResetConfirmSerializer
        permission_classes = (AllowAny,)

        @sensitive_post_parameters_m
        def dispatch(self, *args, **kwargs):
                return super(PasswordResetConfirmView, self).dispatch(*args, **kwargs)

        def post(self, request, *args, **kwargs):
                serializer = self.get_serializer(data=request.data)
                serializer.is_valid(raise_exception=True)
                serializer.save()
                return Response(
                        {"detail": _("Password has been reset with the new password.")}
                )

La ligne serializer.is_valid(raise_exception=True) appellera run_validation De Serializer(BaseSerializer) de rest_framework. Cela utilisera en outre le PasswordResetConfirmSerializer de rest_auth:

# project/.venv/Lib/site-packages/rest_auth/serializers.py
class PasswordResetConfirmSerializer(serializers.Serializer):
        """
        Serializer for requesting a password reset e-mail.
        """
        new_password1 = serializers.CharField(max_length=128)
        new_password2 = serializers.CharField(max_length=128)
        uid = serializers.CharField()
        token = serializers.CharField()

        set_password_form_class = SetPasswordForm

        def custom_validation(self, attrs):
                pass

        def validate(self, attrs):
                self._errors = {}

                # Decode the uidb64 to uid to get User object
                try:
                        uid = force_text(uid_decoder(attrs['uid']))
                        self.user = UserModel._default_manager.get(pk=uid)
                except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
                        raise ValidationError({'uid': ['Invalid value']})

                self.custom_validation(attrs)
                # Construct SetPasswordForm instance
                self.set_password_form = self.set_password_form_class(
                        user=self.user, data=attrs
                )
                if not self.set_password_form.is_valid():
                        raise serializers.ValidationError(self.set_password_form.errors)
                if not default_token_generator.check_token(self.user, attrs['token']):
                        raise ValidationError({'token': ['Invalid value']})

                return attrs

Et comme vous pouvez enfin le voir, cette classe attend un uidb64 au lieu d'un uidb36 pour l'ID utilisateur, et je ne veux même pas savoir si le format de jeton correspond de toute façon à ce qui est attendu ici.

Je ne peux vraiment pas trouver de bonne documentation sur la façon de configurer correctement rest_auth Pour le processus de réinitialisation complète du mot de passe: j'ai obtenu le courrier électronique, mais il me semble que rest_auth Générerait un jeton/réinitialisation incorrect -lien pour ce qu'il attend réellement de l'utilisateur.

Résumé

Je crois que le processus de confirmation de réinitialisation du mot de passe se termine par le bon code backend, tandis que la génération d'e-mails/de jetons est foirée.

Tout ce que je veux, c'est récupérer un uid et un jeton que je peux renvoyer à Django rest-auth afin de permettre aux utilisateurs de réinitialiser leurs mots de passe. Actuellement, il semble que ces uids et jetons sont créés par une bibliothèque et consommés par une autre bibliothèque qui attend et crée des formats de jetons et uids?

Merci d'avance!

Plein settings.py

Voici mon settings.py Complet:

# project/vuedj/settings.py
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_PATH = os.path.realpath(os.path.dirname(__file__))
SECRET_KEY = persisted_settings.SECRET_KEY
DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
CORS_Origin_ALLOW_ALL = True
CORS_URLS_REGEX = r'^/api/.*$'
CORS_ALLOW_CREDENTIALS = True

# Application definition

INSTALLED_APPS = [
    'Django.contrib.admin',
    'Django.contrib.auth',
    'Django.contrib.contenttypes',
    'Django.contrib.sessions',
    'Django.contrib.messages',
    'Django.contrib.staticfiles',
    'Django.contrib.sites',
    'rest_framework',
    'rest_framework.authtoken',
    'corsheaders',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.github',
    'rest_auth',
    'rest_auth.registration',
    'sceneries',
    'accounts',
    'api',
    'app',
]

EMAIL_BACKEND = 'Django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = 'app-messages'
SITE_ID = 1

AUTH_USER_MODEL = 'accounts.User'
ACCOUNT_USER_MODEL_USERNAME_FIELD = 'username'
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'

ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_USERNAME_REQUIRED = True
ACCOUNT_USER_EMAIL_FIELD = 'email'
ACCOUNT_LOGOUT_ON_GET = True
ACCOUNT_FORMS = {"login": "accounts.forms.UserLoginForm"}
LOGIN_REDIRECT_URL = 'home'
LOGIN_URL = 'api/v1/accounts/login/'

CSRF_COOKIE_NAME = "csrftoken"

REST_AUTH_SERIALIZERS = {
    "USER_DETAILS_SERIALIZER": "accounts.serializers.CustomUserDetailsSerializer",
    "LOGIN_SERIALIZER": "accounts.serializers.CustomUserLoginSerializer",
    "PASSWORD_RESET_SERIALIZER": "accounts.serializers.CustomPasswordResetSerializer"
}

REST_AUTH_REGISTER_SERIALIZERS = {
    "REGISTER_SERIALIZER": "accounts.serializers.CustomRegisterSerializer",
}

# Following is added to enable registration with email instead of username
AUTHENTICATION_BACKENDS = (
    # Needed to login by username in Django admin, regardless of `allauth`
    "Django.contrib.auth.backends.ModelBackend",
    # `allauth` specific authentication methods, such as login by e-mail
    "allauth.account.auth_backends.AuthenticationBackend",
)

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'Django.middleware.security.SecurityMiddleware',
    'Django.contrib.sessions.middleware.SessionMiddleware',
    'Django.middleware.common.CommonMiddleware',
    'Django.middleware.csrf.CsrfViewMiddleware',
    'Django.contrib.auth.middleware.AuthenticationMiddleware',
    'Django.contrib.messages.middleware.MessageMiddleware',
    'Django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'vuedj.urls'

TEMPLATES = [
    {
        'BACKEND': 'Django.template.backends.Django.DjangoTemplates',
        'DIRS': [
            'templates/',
            'templates/emails/'
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'Django.template.context_processors.debug',
                'Django.template.context_processors.request',
                'Django.contrib.auth.context_processors.auth',
                'Django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'vuedj.wsgi.application'

try:
        DATABASES = persisted_settings.DATABASES
except AttributeError:
        DATABASES = {
                'default': {
                        'ENGINE': 'Django.db.backends.sqlite3',
                        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
                }
        }

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'Django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'Django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'Django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'Django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)
STATIC_ROOT = os.path.join(BASE_DIR, '../staticfiles/static')
MEDIA_ROOT = os.path.join(BASE_DIR, '../staticfiles/mediafiles')
STATIC_URL = '/static/'
MEDIA_URL = '/media/'

TEST_RUNNER = 'Django_nose.NoseTestSuiteRunner'

NOSE_ARGS = [
    '--with-coverage',
    '--cover-package=app',  # For multiple apps use '--cover-package=foo, bar'
]
3
ElectRocnic

Nous avons la même configuration et je peux vous dire que cela fonctionne mais je ne peux pas vous aider avec la base 36 sauf que même la documentation Django dit que c'est la base 64!

Cependant, vous avez écrit que cette partie théorique n'est pas si importante pour vous et trouvons le point qui vous manque. La configuration est un peu déroutante car vous n'avez pas besoin de tout. Je ne comprends pas exactement où tu es coincé. Par conséquent, je veux vous dire comment je l'ai fait:

J'ai défini l'URL de réinitialisation du mot de passe juste pour Django/allauth pour le trouver lors de la création du lien dans l'e-mail:

from Django.views.generic import TemplateView

PASSWORD_RESET = (
    r'^auth/password-reset-confirmation/'
    r'(?P<uidb64>[0-9A-Za-z_\-]+)/'
    r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})$'
)

urlpatterns += [
    re_path(
        PASSWORD_RESET,
        TemplateView.as_view(),
        name='password_reset_confirm',
    ),
]

Vous n'êtes pas obligé de le faire (parce que vous include('allauth.urls'), vous avez en fait vous n'avez pas besoin de ces URL ) mais je tiens à préciser que cette URL ne pointe pas vers le backend ! Cela dit, laissez votre frontend servir cette URL avec un formulaire pour entrer un nouveau mot de passe, puis utilisez axios ou quelque chose pour POSTuid, token, new_password1 et new_password2 à votre point de terminaison.

Dans votre cas, le point final est

path(
    'reset-password-confirm/',
    acc_views.CustomPasswordResetConfirmView.as_view(),
    name='reset-password-confirm'
),

Est-ce que cela vous aide? Sinon, faites-le moi savoir.

0
yofee