Skip to content

Commit

Permalink
Merge pull request #60 from Hall-Erik/validate_token_with_endpoint
Browse files Browse the repository at this point in the history
Created endpoint for validating a token
  • Loading branch information
anx-ckreuzberger authored Aug 8, 2019
2 parents c1dc82a + c41dec3 commit d6d46fc
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The following endpoints are provided:

* `POST ${API_URL}/reset_password/` - request a reset password token by using the ``email`` parameter
* `POST ${API_URL}/reset_password/confirm/` - using a valid ``token``, the users password is set to the provided ``password``
* `POST ${API_URL}/reset_password/validate_token/` - will return a 200 if a given ``token`` is valid

where `${API_URL}/` is the url specified in your *urls.py* (e.g., `api/password_reset/`)

Expand Down
7 changes: 6 additions & 1 deletion django_rest_passwordreset/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
__all__ = [
'EmailSerializer',
'PasswordTokenSerializer',
'TokenSerializer',
]


Expand All @@ -14,4 +15,8 @@ class EmailSerializer(serializers.Serializer):

class PasswordTokenSerializer(serializers.Serializer):
password = serializers.CharField(label=_("Password"), style={'input_type': 'password'})
token = serializers.CharField()
token = serializers.CharField()


class TokenSerializer(serializers.Serializer):
token = serializers.CharField()
3 changes: 2 additions & 1 deletion django_rest_passwordreset/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
""" URL Configuration for core auth
"""
from django.conf.urls import url, include
from django_rest_passwordreset.views import reset_password_request_token, reset_password_confirm
from django_rest_passwordreset.views import reset_password_request_token, reset_password_confirm, reset_password_validate_token

app_name = 'password_reset'

urlpatterns = [
url(r'^validate_token/', reset_password_validate_token, name="reset-password-validate"),
url(r'^confirm/', reset_password_confirm, name="reset-password-confirm"),
url(r'^', reset_password_request_token, name="reset-password-request"),
]
38 changes: 37 additions & 1 deletion django_rest_passwordreset/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response

from django_rest_passwordreset.serializers import EmailSerializer, PasswordTokenSerializer
from django_rest_passwordreset.serializers import EmailSerializer, PasswordTokenSerializer, TokenSerializer
from django_rest_passwordreset.models import ResetPasswordToken, clear_expired, get_password_reset_token_expiry_time, \
get_password_reset_lookup_field
from django_rest_passwordreset.signals import reset_password_token_created, pre_password_reset, post_password_reset

User = get_user_model()

__all__ = [
'ValidateToken',
'ResetPasswordConfirm',
'ResetPasswordRequestToken',
'reset_password_validate_token',
'reset_password_confirm',
'reset_password_request_token'
]
Expand All @@ -27,6 +29,39 @@
HTTP_IP_ADDRESS_HEADER = getattr(settings, 'DJANGO_REST_PASSWORDRESET_IP_ADDRESS_HEADER', 'REMOTE_ADDR')


class ResetPasswordValidateToken(GenericAPIView):
"""
An Api View which provides a method to verify that a token is valid
"""
throttle_classes = ()
permission_classes = ()
serializer_class = TokenSerializer

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
token = serializer.validated_data['token']

# get token validation time
password_reset_token_validation_time = get_password_reset_token_expiry_time()

# find token
reset_password_token = ResetPasswordToken.objects.filter(key=token).first()

if reset_password_token is None:
return Response({'status': 'notfound'}, status=status.HTTP_404_NOT_FOUND)

# check expiry date
expiry_date = reset_password_token.created_at + timedelta(hours=password_reset_token_validation_time)

if timezone.now() > expiry_date:
# delete expired token
reset_password_token.delete()
return Response({'status': 'expired'}, status=status.HTTP_404_NOT_FOUND)

return Response({'status': 'OK'})


class ResetPasswordConfirm(GenericAPIView):
"""
An Api View which provides a method to reset a password based on a unique token
Expand Down Expand Up @@ -153,5 +188,6 @@ def post(self, request, *args, **kwargs):
return Response({'status': 'OK'})


reset_password_validate_token = ResetPasswordValidateToken.as_view()
reset_password_confirm = ResetPasswordConfirm.as_view()
reset_password_request_token = ResetPasswordRequestToken.as_view()
15 changes: 15 additions & 0 deletions tests/test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def setUpUrls(self):
""" set up urls by using djangos reverse function """
self.reset_password_request_url = reverse('password_reset:reset-password-request')
self.reset_password_confirm_url = reverse('password_reset:reset-password-confirm')
self.reset_password_validate_token_url = reverse('password_reset:reset-password-validate')

def django_check_login(self, username, password):
"""
Expand Down Expand Up @@ -69,3 +70,17 @@ def rest_do_reset_password_with_token(self, token, new_password, HTTP_USER_AGENT
HTTP_USER_AGENT=HTTP_USER_AGENT,
REMOTE_ADDR=REMOTE_ADDR
)

def rest_do_validate_token(self, token, HTTP_USER_AGENT='', REMOTE_ADDR='127.0.0.1'):
""" REST API wrapper for validating a token """
data = {
'token': token
}

return self.client.post(
self.reset_password_validate_token_url,
data,
format='json',
HTTP_USER_AGENT=HTTP_USER_AGENT,
REMOTE_ADDR=REMOTE_ADDR
)
86 changes: 86 additions & 0 deletions tests/test/test_auth_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,92 @@ def test_try_reset_password_email_does_not_exist(self):
# response should have "email" in it
self.assertTrue("email" in decoded_response)

@patch('django_rest_passwordreset.signals.reset_password_token_created.send')
def test_validate_token(self, mock_reset_password_token_created):
""" Tests validate token """

# there should be zero tokens
self.assertEqual(ResetPasswordToken.objects.all().count(), 0)

response = self.rest_do_request_reset_token(email="user1@mail.com")
self.assertEqual(response.status_code, status.HTTP_200_OK)
# check that the signal was sent once
self.assertTrue(mock_reset_password_token_created.called)
self.assertEqual(mock_reset_password_token_created.call_count, 1)
last_reset_password_token = mock_reset_password_token_created.call_args[1]['reset_password_token']
self.assertNotEqual(last_reset_password_token.key, "")

# there should be one token
self.assertEqual(ResetPasswordToken.objects.all().count(), 1)
# and it should be assigned to user1
self.assertEqual(
ResetPasswordToken.objects.filter(key=last_reset_password_token.key).first().user.username,
"user1"
)

# try to validate token
response = self.rest_do_validate_token(last_reset_password_token.key)
self.assertEqual(response.status_code, status.HTTP_200_OK)

# there should be one token
self.assertEqual(ResetPasswordToken.objects.all().count(), 1)

# try to login with the old username/password (should work)
self.assertTrue(
self.django_check_login("user1", "secret1"),
msg="User 1 should still be able to login with the old credentials"
)

def test_validate_bad_token(self):
""" Tests validate an invalid token """

# there should be zero tokens
self.assertEqual(ResetPasswordToken.objects.all().count(), 0)

# try to validate an invalid token
response = self.rest_do_validate_token("not_a_valid_token")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

# there should be zero tokens
self.assertEqual(ResetPasswordToken.objects.all().count(), 0)

@patch('django_rest_passwordreset.signals.reset_password_token_created.send')
@override_settings(DJANGO_REST_MULTITOKENAUTH_RESET_TOKEN_EXPIRY_TIME=-1)
def test_validate_expired_token(self, mock_reset_password_token_created):
""" Tests validate an expired token """

# there should be zero tokens
self.assertEqual(ResetPasswordToken.objects.all().count(), 0)

response = self.rest_do_request_reset_token(email="user1@mail.com")
self.assertEqual(response.status_code, status.HTTP_200_OK)
# check that the signal was sent once
self.assertTrue(mock_reset_password_token_created.called)
self.assertEqual(mock_reset_password_token_created.call_count, 1)
last_reset_password_token = mock_reset_password_token_created.call_args[1]['reset_password_token']
self.assertNotEqual(last_reset_password_token.key, "")

# there should be one token
self.assertEqual(ResetPasswordToken.objects.all().count(), 1)
# and it should be assigned to user1
self.assertEqual(
ResetPasswordToken.objects.filter(key=last_reset_password_token.key).first().user.username,
"user1"
)

# try to validate token
response = self.rest_do_validate_token(last_reset_password_token.key)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

# there should be zero tokens
self.assertEqual(ResetPasswordToken.objects.all().count(), 0)

# try to login with the old username/password (should work)
self.assertTrue(
self.django_check_login("user1", "secret1"),
msg="User 1 should still be able to login with the old credentials"
)

@patch('django_rest_passwordreset.signals.reset_password_token_created.send')
def test_reset_password(self, mock_reset_password_token_created):
""" Tests resetting a password """
Expand Down

0 comments on commit d6d46fc

Please sign in to comment.