Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

778 fe incorporate captcha #791

Merged
merged 29 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
57617db
Updated settings and env for recaptcha setup usage
superolegatron Sep 12, 2024
c9076ac
created function to verify recaptcha from server side
superolegatron Sep 12, 2024
a20012a
Updated signup and signin serializers to validate recaptcha
superolegatron Sep 12, 2024
70d4531
updated dependencies and env sample to support recaptcha
superolegatron Sep 12, 2024
4f5b5cf
Created loader to use recaptcha only when its needed
superolegatron Sep 12, 2024
94b3c18
Updated signin and signup to work with recaptcha
superolegatron Sep 12, 2024
b8bdf2b
Unrelated to task, small fix to avoid error in FE console
superolegatron Sep 12, 2024
d61fdc2
small fix
superolegatron Sep 12, 2024
429fa54
Merge branch 'develop' into 768-be-incorporate-captcha
superolegatron Sep 12, 2024
1d98b64
fixed tests
superolegatron Sep 13, 2024
6cc4bd6
Merge branch '768-be-incorporate-captcha' of https://github.com/ita-s…
superolegatron Sep 13, 2024
aecad9f
minor fixes
superolegatron Sep 13, 2024
312bc82
recaptcha versioning
superolegatron Sep 15, 2024
099e21b
recaptcha versioning
superolegatron Sep 15, 2024
3b7bbec
recaptcha versioning
superolegatron Sep 15, 2024
633b9a4
Merge branch '778-fe-incorporate-captcha' of https://github.com/ita-s…
superolegatron Sep 15, 2024
ee23c79
minor fixes
superolegatron Sep 15, 2024
aaf91d4
moved url for captcha verification to env
superolegatron Sep 15, 2024
9ce9e35
updated sample.env
superolegatron Sep 16, 2024
b618bdc
minor updates
superolegatron Sep 16, 2024
80ef38e
minor updates
superolegatron Sep 16, 2024
0350888
Merge pull request #781 from ita-social-projects/768-be-incorporate-c…
superolegatron Sep 20, 2024
a196e69
small fix to properly handle incorrect form submitting with captcha
superolegatron Sep 24, 2024
f67caaa
added captcha token reset for signup
superolegatron Sep 24, 2024
1ae63cb
adjusted test according to updated verification
superolegatron Sep 24, 2024
5eb01be
small fix
superolegatron Sep 24, 2024
3006e14
merged develop and resolved conflicts, added captcha variables to doc…
superolegatron Sep 25, 2024
18943c2
deleted redundant code
superolegatron Sep 27, 2024
6fcc1c1
fixed uncaught in promise timeout
superolegatron Oct 1, 2024
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
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
Loading