diff --git a/apiserver/plane/authentication/utils/mobile/login.py b/apiserver/plane/authentication/utils/mobile/login.py index eda125e9a2..070cf4a065 100644 --- a/apiserver/plane/authentication/utils/mobile/login.py +++ b/apiserver/plane/authentication/utils/mobile/login.py @@ -12,7 +12,7 @@ from plane.settings.redis import redis_instance from plane.authentication.utils.host import base_host -def generate_random_string(length=32): +def generate_random_string(length=64): return "".join( random.choices( string.ascii_uppercase + string.ascii_lowercase + string.digits, diff --git a/apiserver/plane/authentication/views/app/mobile/email.py b/apiserver/plane/authentication/views/app/mobile/email.py index b4c78a342a..451538a28e 100644 --- a/apiserver/plane/authentication/views/app/mobile/email.py +++ b/apiserver/plane/authentication/views/app/mobile/email.py @@ -11,7 +11,6 @@ from django.views import View from plane.authentication.provider.credentials.email import EmailProvider from plane.authentication.utils.mobile.login import ( ValidateAuthToken, - mobile_user_login, mobile_validate_user_onboarding, ) from plane.license.models import Instance @@ -127,13 +126,8 @@ class MobileSignInAuthEndpoint(View): return HttpResponseRedirect(url) # Login the user and record his device info - session = mobile_user_login( - request=request, user=user, is_app=True - ) - session_key = session.session_key - session_token = ValidateAuthToken() - session_token.set_value(session_key) + session_token.set_value(str(user.id)) # redirect to referrer path url = urljoin( diff --git a/apiserver/plane/authentication/views/app/mobile/magic.py b/apiserver/plane/authentication/views/app/mobile/magic.py index cbf866cdf5..e2883d5c48 100644 --- a/apiserver/plane/authentication/views/app/mobile/magic.py +++ b/apiserver/plane/authentication/views/app/mobile/magic.py @@ -12,7 +12,6 @@ from plane.authentication.provider.credentials.magic_code import ( ) from plane.authentication.utils.mobile.login import ( ValidateAuthToken, - mobile_user_login, mobile_validate_user_onboarding, ) from plane.authentication.utils.user_auth_workflow import ( @@ -110,13 +109,8 @@ class MobileMagicSignInEndpoint(View): return HttpResponseRedirect(url) # Login the user and record his device info - session = mobile_user_login( - request=request, user=user, is_app=True - ) - session_key = session.session_key - session_token = ValidateAuthToken() - session_token.set_value(session_key) + session_token.set_value(str(user.id)) # redirect to referrer path url = urljoin( diff --git a/apiserver/plane/authentication/views/app/mobile/token.py b/apiserver/plane/authentication/views/app/mobile/token.py index c59d0b8c08..80939e9d1d 100644 --- a/apiserver/plane/authentication/views/app/mobile/token.py +++ b/apiserver/plane/authentication/views/app/mobile/token.py @@ -1,16 +1,15 @@ -# Django imports -from django.conf import settings - # Third party imports from rest_framework import status from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework_simplejwt.tokens import RefreshToken # Module imports from plane.authentication.utils.mobile.login import ( ValidateAuthToken, ) +from plane.db.models import User class MobileSessionTokenCheckEndpoint(APIView): @@ -18,6 +17,15 @@ class MobileSessionTokenCheckEndpoint(APIView): AllowAny, ] + def get_tokens_for_user(self, user): + # Get the refresh token + refresh = RefreshToken.for_user(user) + # Return the tokens + return { + "refresh_token": str(refresh), + "access_token": str(refresh.access_token), + } + def post(self, request): try: token = request.data.get("token", False) @@ -48,12 +56,22 @@ class MobileSessionTokenCheckEndpoint(APIView): # Remove the token session_token.remove_token() - response_session = { - "session_name": settings.SESSION_COOKIE_NAME, - "session_id": session_details.get("session_id"), - } + # Get the user id + user_id = session_details.get("session_id") - return Response(response_session, status=status.HTTP_200_OK) + # Get the user + user = User.objects.filter(id=user_id).first() + # Check if user exists + if not user: + return Response( + {"error": "User not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + # Get the tokens + response = self.get_tokens_for_user(user) + # Return the tokens + return Response(response, status=status.HTTP_200_OK) except Exception: return Response( {"error": "Something went wrong"}, diff --git a/apiserver/plane/graphql/permissions/project.py b/apiserver/plane/graphql/permissions/project.py index 48d7a0ad87..9157cf57be 100644 --- a/apiserver/plane/graphql/permissions/project.py +++ b/apiserver/plane/graphql/permissions/project.py @@ -4,8 +4,6 @@ from typing import Any # Third-Party Imports from asgiref.sync import sync_to_async -# Django Imports -from django.contrib.auth import get_user # Strawberry Imports from strawberry.types import Info @@ -30,9 +28,7 @@ class IsAuthenticated(BasePermission): } async def has_permission(self, source: Any, info: Info, **kwargs) -> bool: - self.user = await sync_to_async(get_user, thread_sensitive=True)( - info.context.request - ) + self.user = info.context.user return self.user.is_authenticated diff --git a/apiserver/plane/graphql/permissions/workspace.py b/apiserver/plane/graphql/permissions/workspace.py index 14441b4c97..55bab7fa6b 100644 --- a/apiserver/plane/graphql/permissions/workspace.py +++ b/apiserver/plane/graphql/permissions/workspace.py @@ -4,8 +4,6 @@ from typing import Any # Third-Party Imports from asgiref.sync import sync_to_async -# Django Imports -from django.contrib.auth import get_user # Strawberry Imports from strawberry.types import Info @@ -30,9 +28,7 @@ class IsAuthenticated(BasePermission): } async def has_permission(self, source: Any, info: Info, **kwargs) -> bool: - self.user = await sync_to_async(get_user, thread_sensitive=True)( - info.context.request - ) + self.user = info.context.user return self.user.is_authenticated diff --git a/apiserver/plane/graphql/views.py b/apiserver/plane/graphql/views.py index 1f05230a19..5d101c5110 100644 --- a/apiserver/plane/graphql/views.py +++ b/apiserver/plane/graphql/views.py @@ -1,12 +1,11 @@ -# Django imports -from django.contrib.auth import get_user - from typing import Any, Dict, Optional from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt # Third-Party Imports from asgiref.sync import sync_to_async +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.exceptions import InvalidToken, TokenError # Strawberry imports from strawberry.django.views import AsyncGraphQLView @@ -14,15 +13,46 @@ from strawberry.types import ExecutionResult from strawberry.types.execution import ExecutionContext +# Sync to async function to get the validated token +@sync_to_async +def get_validated_token(token): + return JWTAuthentication().get_validated_token(token) + + +# Sync to async function to get the user +@sync_to_async +def get_jwt_user(validated_token): + return JWTAuthentication().get_user(validated_token) + + @method_decorator(csrf_exempt, name="dispatch") class CustomGraphQLView(AsyncGraphQLView): async def get_context(self, request, response): - # Get the user - user = await sync_to_async(get_user)(request) - - # Add the user to the context + # Get the context from the parent class context = await super().get_context(request, response) - context.user = user + try: + # Get the token from the request headers + auth_header = request.headers.get("Authorization") + + # If the token is present, validate it and get the user + if auth_header: + bearer_token = auth_header.split()[1] + if not bearer_token: + context.user = None + + validated_token = await get_validated_token(bearer_token) + if validated_token is None: + context.user = None + + # Get the user from the validated token + user = await get_jwt_user(validated_token) + + # Set the user in the context + context.user = user + else: + context.user = None + except (InvalidToken, TokenError) as e: + context.user = None return context async def process_result( diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 3697344f17..6c15f322fd 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -4,6 +4,8 @@ import os from urllib.parse import urlparse +from datetime import timedelta + # Third party imports import dj_database_url @@ -401,3 +403,25 @@ IS_MULTI_TENANT = os.environ.get("IS_MULTI_TENANT", "0") == "1" # Instance Changelog URL INSTANCE_CHANGELOG_URL = os.environ.get("INSTANCE_CHANGELOG_URL", "") + +# JWT Settings +SIMPLE_JWT = { + # The number of seconds the access token will be valid + "ACCESS_TOKEN_LIFETIME": timedelta( + days=int(os.environ.get("ACCESS_TOKEN_LIFETIME", 1)) + ), + # The number of seconds the refresh token will be valid + "REFRESH_TOKEN_LIFETIME": timedelta( + days=int(os.environ.get("REFRESH_TOKEN_LIFETIME", 7)) + ), + # The number of seconds the refresh token will be valid + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, + # Algorithm used to sign the token + "ALGORITHM": "HS256", + "SIGNING_KEY": SECRET_KEY, + "AUTH_HEADER_TYPES": ("Bearer",), + "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", + "USER_ID_FIELD": "id", + "USER_ID_CLAIM": "user_id", +} diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index d4d660f8e9..f405d46b84 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -70,3 +70,5 @@ python3-saml==1.16.0 openfeature-sdk==0.7.0 # graphql strawberry-graphql-django==0.48.0 +# django rest framework jwt +djangorestframework-simplejwt==5.3.1 \ No newline at end of file diff --git a/web/ee/components/mobile/authentication/forms/email-confirmation.tsx b/web/ee/components/mobile/authentication/forms/email-confirmation.tsx index 69ac121531..5877d7e23e 100644 --- a/web/ee/components/mobile/authentication/forms/email-confirmation.tsx +++ b/web/ee/components/mobile/authentication/forms/email-confirmation.tsx @@ -14,8 +14,6 @@ import { } from "@/helpers/authentication.helper"; import { cn } from "@/helpers/common.helper"; import { checkEmailValidity } from "@/helpers/string.helper"; -// plane web services -import mobileAuthService from "@/plane-web/services/mobile.service"; // services import { AuthService } from "@/services/auth.service"; @@ -49,28 +47,9 @@ export const MobileAuthEmailValidationForm: FC = const handleFormSubmit = async (event: FormEvent) => { event.preventDefault(); - let isUserSignOut = false; - try { - await mobileAuthService.currentUser(); - isUserSignOut = true; - } catch (error) { - console.error(error); - } - - let isEmailShouldBeVerified = true; - if (isUserSignOut) { - try { - await mobileAuthService.signOut(); - } catch (error) { - console.error(error); - isEmailShouldBeVerified = false; - } - } - - if (!isEmailShouldBeVerified) return; - handleEmail(email); setIsSubmitting(true); + const payload: IEmailCheckData = { email: email, };