Skip to content

Commit

Permalink
Merge pull request #791 from ita-social-projects/778-fe-incorporate-c…
Browse files Browse the repository at this point in the history
…aptcha

778 fe incorporate captcha
  • Loading branch information
superolegatron authored Oct 2, 2024
2 parents 0c3778f + 6fcc1c1 commit 88da85c
Show file tree
Hide file tree
Showing 17 changed files with 179 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/tests_be.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ env:
CORS_ORIGIN_WHITELIST: ''
ALLOWED_ENV_HOST: ''
REDIS_URL: ${{ vars.REDIS_URL }}
RECAPTCHA_V2_PRIVATE_KEY: ''
RECAPTCHA_URL: ${{ vars.RECAPTCHA_URL }}


jobs:
Expand Down
23 changes: 22 additions & 1 deletion BackEnd/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
validate_password_include_symbols,
)
from validation.validate_profile import validate_profile
from validation.validate_recaptcha import verify_recaptcha

User = get_user_model()

Expand All @@ -42,13 +43,17 @@ class UserRegistrationSerializer(UserCreatePasswordRetypeSerializer):
password = serializers.CharField(
style={"input_type": "password"}, write_only=True
)
captcha = serializers.CharField(
write_only=True, allow_blank=True, allow_null=True
)

class Meta(UserCreatePasswordRetypeSerializer.Meta):
model = User
fields = ("email", "password", "name", "surname", "company")
fields = ("email", "password", "name", "surname", "company", "captcha")

def validate(self, value):
custom_errors = defaultdict(list)
captcha_token = value.get("captcha")
self.fields.pop("re_password", None)
re_password = value.pop("re_password")
email = value.get("email").lower()
Expand All @@ -74,11 +79,16 @@ def validate(self, value):
custom_errors["password"].append(error.message)
if value["password"] != re_password:
custom_errors["password"].append("Passwords don't match.")
if captcha_token and not verify_recaptcha(captcha_token):
custom_errors["captcha"].append(
"Invalid reCAPTCHA. Please try again."
)
if custom_errors:
raise serializers.ValidationError(custom_errors)
return value

def create(self, validated_data):
validated_data.pop("captcha", None)
company_data = validated_data.pop("company")
user = User.objects.create(**validated_data)
user.set_password(validated_data["password"])
Expand All @@ -105,7 +115,13 @@ class Meta(UserSerializer.Meta):


class CustomTokenCreateSerializer(TokenCreateSerializer):
captcha = serializers.CharField(
write_only=True, allow_blank=True, allow_null=True
)

def validate(self, attrs):
captcha_token = attrs.get("captcha")

try:
validate_profile(attrs.get("email"))
except ValidationError as error:
Expand All @@ -116,6 +132,11 @@ def validate(self, attrs):
except RateLimitException:
self.fail("inactive_account")

if captcha_token and not verify_recaptcha(captcha_token):
raise serializers.ValidationError(
"Invalid reCAPTCHA. Please try again."
)

@RateLimitDecorator(
calls=django_settings.ATTEMPTS_FOR_LOGIN,
period=django_settings.DELAY_FOR_LOGIN,
Expand Down
9 changes: 9 additions & 0 deletions BackEnd/authentication/tests/test_user_autologout.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import timedelta
from unittest.mock import patch

from rest_framework import status
from rest_framework.authtoken.models import Token
Expand All @@ -12,6 +13,12 @@

class UserLogoutAPITests(APITestCase):
def setUp(self):
patcher = patch(
"authentication.serializers.verify_recaptcha", return_value=True
)
self.mock_verify_recaptcha = patcher.start()
self.addCleanup(patcher.stop)

self.user = UserFactory(
email="test@test.com", name="Test", surname="Test"
)
Expand All @@ -29,6 +36,7 @@ def test_user_autologout_after_14_days(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
).data["auth_token"]
token = Token.objects.get(key=self.test_user_token)
Expand All @@ -53,6 +61,7 @@ def test_user_autologout_after_10_days(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
).data["auth_token"]
token = Token.objects.get(key=self.test_user_token)
Expand Down
18 changes: 18 additions & 0 deletions BackEnd/authentication/tests/test_user_login.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

from rest_framework import status
from rest_framework.test import APITestCase
from time import sleep
Expand All @@ -9,6 +11,12 @@

class UserLoginAPITests(APITestCase):
def setUp(self):
patcher = patch(
"authentication.serializers.verify_recaptcha", return_value=True
)
self.mock_verify_recaptcha = patcher.start()
self.addCleanup(patcher.stop)

self.user = UserFactory(email="test@test.com")

def test_login_successful(self):
Expand All @@ -20,6 +28,7 @@ def test_login_successful(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
Expand All @@ -35,6 +44,7 @@ def test_login_email_incorrect(self):
data={
"email": "tost@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
Expand All @@ -56,6 +66,7 @@ def test_login_password_incorrect(self):
data={
"email": "test@test.com",
"password": "Test5678",
"captcha": "dummy_captcha",
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
Expand All @@ -77,13 +88,15 @@ def test_login_after_allowed_number_attempts(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)
self.client.post(
path="/api/auth/token/login/",
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)

Expand All @@ -92,6 +105,7 @@ def test_login_after_allowed_number_attempts(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)
sleep(6)
Expand All @@ -110,6 +124,7 @@ def test_login_after_allowed_delay_time(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)

Expand All @@ -118,13 +133,15 @@ def test_login_after_allowed_delay_time(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)
self.client.post(
path="/api/auth/token/login/",
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)
sleep(6)
Expand All @@ -133,6 +150,7 @@ def test_login_after_allowed_delay_time(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)

Expand Down
9 changes: 9 additions & 0 deletions BackEnd/authentication/tests/test_user_logout.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

from rest_framework import status
from rest_framework.test import APITestCase

Expand All @@ -8,6 +10,12 @@

class UserLogoutAPITests(APITestCase):
def setUp(self):
patcher = patch(
"authentication.serializers.verify_recaptcha", return_value=True
)
self.mock_verify_recaptcha = patcher.start()
self.addCleanup(patcher.stop)

self.user = UserFactory(email="test@test.com")

def test_logout_successful(self):
Expand All @@ -19,6 +27,7 @@ def test_logout_successful(self):
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
).data["auth_token"]
self.client.credentials(
Expand Down
15 changes: 15 additions & 0 deletions BackEnd/authentication/tests/test_user_registration.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

from rest_framework import status
from rest_framework.test import APITestCase

Expand All @@ -8,6 +10,12 @@

class UserRegistrationAPITests(APITestCase):
def setUp(self):
patcher = patch(
"authentication.serializers.verify_recaptcha", return_value=True
)
self.mock_verify_recaptcha = patcher.start()
self.addCleanup(patcher.stop)

self.user = UserFactory(email="test@test.com")

def test_register_user_yurosoba_successful(self):
Expand All @@ -19,6 +27,7 @@ def test_register_user_yurosoba_successful(self):
"re_password": "Test1234",
"name": "Jane",
"surname": "Smith",
"captcha": "dummy_captcha",
"company": {
"name": "My Company",
"is_registered": True,
Expand Down Expand Up @@ -49,6 +58,7 @@ def test_register_user_fop_successful(self):
"re_password": "Test1234",
"name": "Jane",
"surname": "Smith",
"captcha": "dummy_captcha",
"company": {
"name": "My Company",
"is_registered": True,
Expand Down Expand Up @@ -79,6 +89,7 @@ def test_register_user_email_incorrect(self):
"re_password": "Test1234",
"name": "Jane",
"surname": "Smith",
"captcha": "dummy_captcha",
"company": {
"name": "My Company",
"is_registered": True,
Expand All @@ -103,6 +114,7 @@ def test_register_user_email_exists(self):
"re_password": "Test1234",
"name": "Test",
"surname": "Test",
"captcha": "dummy_captcha",
"company": {
"name": "Test Company",
"is_registered": True,
Expand All @@ -127,6 +139,7 @@ def test_register_user_password_incorrect(self):
"re_password": "tess",
"name": "Jane",
"surname": "Smith",
"captcha": "dummy_captcha",
"company": {
"name": "My Company",
"is_registered": True,
Expand Down Expand Up @@ -157,6 +170,7 @@ def test_register_user_who_represent_empty_fields(self):
"re_password": "Test1234",
"name": "Jane",
"surname": "Smith",
"captcha": "dummy_captcha",
"company": {
"name": "My Company",
"is_registered": False,
Expand All @@ -181,6 +195,7 @@ def test_register_user_who_represent_both_chosen(self):
"re_password": "Test1234",
"name": "Jane",
"surname": "Smith",
"captcha": "dummy_captcha",
"company": {
"name": "My Company",
"is_registered": True,
Expand Down
4 changes: 4 additions & 0 deletions BackEnd/forum/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ def show_toolbar(request):
},
}

# ReCaptcha V2 Invisible
RECAPTCHA_V2_PRIVATE_KEY = config("RECAPTCHA_V2_PRIVATE_KEY")
RECAPTCHA_URL = config("RECAPTCHA_URL")

CONTACTS_INFO = {
"email": "craft.forum0@gmail.com",
"phone": "+38 050 234 23 23",
Expand Down
5 changes: 4 additions & 1 deletion BackEnd/profiles/tests/test_reject_moderation_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ def test_login_blocked_user_due_to_rejected_request(
data={
"email": "test@test.com",
"password": "Test1234",
"captcha": "dummy_captcha",
},
)
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
Expand All @@ -439,8 +440,9 @@ def test_login_blocked_user_due_to_rejected_request(
mock_schedule.assert_called_once()
mock_revoke.assert_called_once()

@patch("authentication.serializers.verify_recaptcha", return_value=True)
def test_register_blocked_user_due_to_rejected_request(
self, mock_revoke, mock_schedule
self, mock_revoke, mock_schedule, mock_verify_recaptcha
):
# user updates both banner and logo
self.user_client.patch(
Expand Down Expand Up @@ -475,6 +477,7 @@ def test_register_blocked_user_due_to_rejected_request(
"re_password": "Test1234",
"name": "Test",
"surname": "Test",
"captcha": "dummy_captcha",
"company": {
"name": "Test Company",
"is_registered": True,
Expand Down
16 changes: 16 additions & 0 deletions BackEnd/validation/validate_recaptcha.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.conf import settings
import requests


def verify_recaptcha(token):
"""
Validates the reCAPTCHA token with Google's API.
"""
recaptcha_url = settings.RECAPTCHA_URL
recaptcha_data = {
"secret": settings.RECAPTCHA_V2_PRIVATE_KEY,
"response": token,
}
response = requests.post(recaptcha_url, data=recaptcha_data)
result = response.json()
return result.get("success", False)
Loading

0 comments on commit 88da85c

Please sign in to comment.