From 249e71e424e9ccb0baee6ac2dc98d0f0a85b10ca Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:20:57 +0530 Subject: [PATCH] fix: email validation (#4707) * fix: email validation on complete login or sign up functionality * dev: add try catch block * dev: split up code * dev: empty return --- .../plane/authentication/adapter/base.py | 181 ++++++++++++------ .../plane/authentication/adapter/error.py | 2 + .../plane/authentication/adapter/oauth.py | 10 +- 3 files changed, 129 insertions(+), 64 deletions(-) diff --git a/apiserver/plane/authentication/adapter/base.py b/apiserver/plane/authentication/adapter/base.py index 7b899e63c..5876e934f 100644 --- a/apiserver/plane/authentication/adapter/base.py +++ b/apiserver/plane/authentication/adapter/base.py @@ -4,6 +4,8 @@ import uuid # Django imports from django.utils import timezone +from django.core.validators import validate_email +from django.core.exceptions import ValidationError # Third party imports from zxcvbn import zxcvbn @@ -46,68 +48,71 @@ class Adapter: def authenticate(self): raise NotImplementedError - def complete_login_or_signup(self): - email = self.user_data.get("email") - user = User.objects.filter(email=email).first() - # Check if sign up case or login - is_signup = bool(user) - if not user: - # New user - (ENABLE_SIGNUP,) = get_configuration_value( - [ - { - "key": "ENABLE_SIGNUP", - "default": os.environ.get("ENABLE_SIGNUP", "1"), - }, - ] - ) - if ( - ENABLE_SIGNUP == "0" - and not WorkspaceMemberInvite.objects.filter( - email=email, - ).exists() - ): - raise AuthenticationException( - error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"], - error_message="SIGNUP_DISABLED", - payload={"email": email}, - ) - user = User(email=email, username=uuid.uuid4().hex) - - if self.user_data.get("user").get("is_password_autoset"): - user.set_password(uuid.uuid4().hex) - user.is_password_autoset = True - user.is_email_verified = True - else: - # Validate password - results = zxcvbn(self.code) - if results["score"] < 3: - raise AuthenticationException( - error_code=AUTHENTICATION_ERROR_CODES[ - "INVALID_PASSWORD" - ], - error_message="INVALID_PASSWORD", - payload={"email": email}, - ) - - user.set_password(self.code) - user.is_password_autoset = False - - avatar = self.user_data.get("user", {}).get("avatar", "") - first_name = self.user_data.get("user", {}).get("first_name", "") - last_name = self.user_data.get("user", {}).get("last_name", "") - user.avatar = avatar if avatar else "" - user.first_name = first_name if first_name else "" - user.last_name = last_name if last_name else "" - user.save() - Profile.objects.create(user=user) - - if not user.is_active: + def sanitize_email(self, email): + # Check if email is present + if not email: raise AuthenticationException( - AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"], - error_message="USER_ACCOUNT_DEACTIVATED", + error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"], + error_message="INVALID_EMAIL", + payload={"email": email}, ) + # Sanitize email + email = str(email).lower().strip() + + # validate email + try: + validate_email(email) + except ValidationError: + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"], + error_message="INVALID_EMAIL", + payload={"email": email}, + ) + # Return email + return email + + def validate_password(self, email): + """Validate password strength""" + results = zxcvbn(self.code) + if results["score"] < 3: + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"], + error_message="INVALID_PASSWORD", + payload={"email": email}, + ) + return + + def __check_signup(self, email): + """Check if sign up is enabled or not and raise exception if not enabled""" + + # Get configuration value + (ENABLE_SIGNUP,) = get_configuration_value( + [ + { + "key": "ENABLE_SIGNUP", + "default": os.environ.get("ENABLE_SIGNUP", "1"), + }, + ] + ) + + # Check if sign up is disabled and invite is present or not + if ( + ENABLE_SIGNUP == "0" + and not WorkspaceMemberInvite.objects.filter( + email=email, + ).exists() + ): + # Raise exception + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"], + error_message="SIGNUP_DISABLED", + payload={"email": email}, + ) + + return True + + def save_user_data(self, user): # Update user details user.last_login_medium = self.provider user.last_active = timezone.now() @@ -116,7 +121,63 @@ class Adapter: user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT") user.token_updated_at = timezone.now() user.save() + return user + def complete_login_or_signup(self): + # Get email + email = self.user_data.get("email") + + # Sanitize email + email = self.sanitize_email(email) + + # Check if the user is present + user = User.objects.filter(email=email).first() + # Check if sign up case or login + is_signup = bool(user) + # If user is not present, create a new user + if not user: + # New user + self.__check_signup(email) + + # Initialize user + user = User(email=email, username=uuid.uuid4().hex) + + # Check if password is autoset + if self.user_data.get("user").get("is_password_autoset"): + user.set_password(uuid.uuid4().hex) + user.is_password_autoset = True + user.is_email_verified = True + + # Validate password + else: + # Validate password + self.validate_password(email) + # Set password + user.set_password(self.code) + user.is_password_autoset = False + + # Set user details + avatar = self.user_data.get("user", {}).get("avatar", "") + first_name = self.user_data.get("user", {}).get("first_name", "") + last_name = self.user_data.get("user", {}).get("last_name", "") + user.avatar = avatar if avatar else "" + user.first_name = first_name if first_name else "" + user.last_name = last_name if last_name else "" + user.save() + + # Create profile + Profile.objects.create(user=user) + + if not user.is_active: + raise AuthenticationException( + AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"], + error_message="USER_ACCOUNT_DEACTIVATED", + ) + + # Save user data + user = self.save_user_data(user=user) + + # Call callback if present if self.callback: self.callback( user, @@ -124,7 +185,9 @@ class Adapter: self.request, ) + # Create or update account if token data is present if self.token_data: self.create_update_account(user=user) + # Return user return user diff --git a/apiserver/plane/authentication/adapter/error.py b/apiserver/plane/authentication/adapter/error.py index 7b12db945..55ff10988 100644 --- a/apiserver/plane/authentication/adapter/error.py +++ b/apiserver/plane/authentication/adapter/error.py @@ -58,6 +58,8 @@ AUTHENTICATION_ERROR_CODES = { "ADMIN_USER_DEACTIVATED": 5190, # Rate limit "RATE_LIMIT_EXCEEDED": 5900, + # Unknown + "AUTHENTICATION_FAILED": 5999, } diff --git a/apiserver/plane/authentication/adapter/oauth.py b/apiserver/plane/authentication/adapter/oauth.py index a917c002a..b1a92e79e 100644 --- a/apiserver/plane/authentication/adapter/oauth.py +++ b/apiserver/plane/authentication/adapter/oauth.py @@ -81,11 +81,11 @@ class OauthAdapter(Adapter): response.raise_for_status() return response.json() except requests.RequestException: - code = ( - "GOOGLE_OAUTH_PROVIDER_ERROR" - if self.provider == "google" - else "GITHUB_OAUTH_PROVIDER_ERROR" - ) + if self.provider == "google": + code = "GOOGLE_OAUTH_PROVIDER_ERROR" + if self.provider == "github": + code = "GITHUB_OAUTH_PROVIDER_ERROR" + raise AuthenticationException( error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code),