Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ describe('account store', () => {
const passwordData = {
new_password1: 'testing password',
new_password2: 'testing password',
token: 'testing token',
uidb64: 'testing uidb64',
};
return store.dispatch('account/setPassword', passwordData).then(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export default {
sendPasswordResetLink(context, email) {
return client.post(window.Urls.auth_password_reset(), { email });
},
setPassword(context, { uidb64, token, new_password1, new_password2 }) {
setPassword(context, { uidb64, new_password1, new_password2 }) {
const data = {
new_password1,
new_password2,
};
return client.post(window.Urls.auth_password_reset_confirm(uidb64, token), data, {
// Django's PasswordResetConfirmView.reset_url_token
// https://docs.djangoproject.com/en/3.2/topics/auth/default/#django.contrib.auth.views.PasswordResetConfirmView
return client.post(window.Urls.auth_password_reset_confirm(uidb64, 'set-password'), data, {
headers: {
'Content-type': 'application/form-url-encode',
},
Expand Down
82 changes: 82 additions & 0 deletions contentcuration/contentcuration/tests/views/test_users.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import json

from django.contrib.auth.tokens import default_token_generator
from django.db import IntegrityError
from django.http.response import HttpResponseBadRequest
from django.http.response import HttpResponseForbidden
from django.http.response import HttpResponseNotAllowed
from django.http.response import HttpResponseRedirectBase
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from mock import mock
from mock import patch

Expand All @@ -15,6 +19,7 @@
from contentcuration.tests.base import StudioAPITestCase
from contentcuration.views.users import login
from contentcuration.views.users import UserActivationView
from contentcuration.views.users import UserPasswordResetConfirmView


class LoginTestCase(StudioAPITestCase):
Expand Down Expand Up @@ -179,6 +184,83 @@ def test_post__handles_integrity_error_gracefully(self, mock_register):
self.assertIn("email", error_data)


class UserPasswordResetConfirmViewTestCase(StudioAPITestCase):
def setUp(self):
super(UserPasswordResetConfirmViewTestCase, self).setUp()
self.user = testdata.user(email="tester@tester.com")
self.user.set_password("old_password")
self.user.save()
self.uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
self.token = default_token_generator.make_token(self.user)

def _url(self, token):
return reverse(
"auth_password_reset_confirm",
kwargs=dict(uidb64=self.uidb64, token=token),
)

def _get(self, token):
response = self.client.get(self._url(token))
if response.url == self._url(UserPasswordResetConfirmView.reset_url_token):
response = self.client.get(response.url)
return response

def _post(self, password="new_password", confirm=None):
data = dict(new_password1=password, new_password2=confirm or password)
return self.client.post(
self._url(UserPasswordResetConfirmView.reset_url_token),
data,
format="json",
)

def _assert_password(self, password):
self.user.refresh_from_db()
self.assertTrue(self.user.check_password(password))

def test_get__valid_token(self):
response = self._get(self.token)
self.assertEqual(
response.url, "/accounts/#/reset-password?uidb64={}".format(self.uidb64)
)

def test_get__invalid_token(self):
response = self._get("invalid-token")
self.assertEqual(response.url, "/accounts/#/reset-expired")

def test_post__valid_session(self):
self._get(self.token)
response = self._post()
self._assert_password("new_password")
self.assertEqual(response.url, "/accounts/#/password-reset-success")

def test_post__no_session(self):
response = self._post()
self._assert_password("old_password")
self.assertIsInstance(response, HttpResponseForbidden)

def test_post__invalid_token_in_url(self):
self.client.post(
self._url("invalid-token"),
dict(new_password1="new_password", new_password2="new_password"),
format="json",
)
self._assert_password("old_password")

def test_post__invalid_form(self):
self._get(self.token)
response = self._post("new_password", "other_password")
self._assert_password("old_password")
self.assertIsInstance(response, HttpResponseForbidden)

def test_post__reused_token(self):
self._get(self.token)
self._post()
self._get(self.token)
response = self._post("second_password")
self._assert_password("new_password")
self.assertIsInstance(response, HttpResponseForbidden)


class UserActivationViewTestCase(StudioAPITestCase):
def setUp(self):
super(UserActivationViewTestCase, self).setUp()
Expand Down
41 changes: 13 additions & 28 deletions contentcuration/contentcuration/views/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.debug import sensitive_post_parameters
from django_registration.backends.activation.views import ActivationView
from django_registration.backends.activation.views import RegistrationView
from rest_framework.authentication import BasicAuthentication
Expand Down Expand Up @@ -318,37 +315,25 @@ def post(self, request):

class UserPasswordResetConfirmView(PasswordResetConfirmView):
http_method_names = ["get", "post"]
success_url = "/accounts/#/password-reset-success"

@method_decorator(sensitive_post_parameters())
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
response = super(UserPasswordResetConfirmView, self).dispatch(
request, *args, **kwargs
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if self.request.method == "POST":
kwargs["data"] = json.loads(self.request.body)
return kwargs

if request.method == "POST":
return self.post(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
return redirect("/accounts/#/reset-password?uidb64={}".format(kwargs["uidb64"]))

# Token is valid, redirect to password reset page
if response.status_code == 302:
return redirect(
"/accounts/#/reset-password?uidb64={}&token={}".format(
kwargs["uidb64"], kwargs["token"]
)
)
def form_invalid(self, form):
return HttpResponseForbidden()

def render_to_response(self, context, **response_kwargs):
if self.request.method == "POST":
return HttpResponseForbidden()
return redirect("/accounts/#/reset-expired")

def get_success_url(self):
return "/accounts/#/password-reset-success"

def post(self, request, *args, **kwargs):
form = self.form_class(self.user, json.loads(request.body))

if form.is_valid():
return self.form_valid(form)
return HttpResponseForbidden()


def request_activation_link(request):
if request.method != "POST":
Expand Down
Loading