[MOBIL-365] fix: migrate session-based authentication to token-based authentication using JWT (#1473)

* chore: updated user authentication from session to jwt

* chore: updated just version in base.txt

* fix: fixed auth validation in graphql context

* fix: fixed auth validation
This commit is contained in:
guru_sainath
2024-10-12 22:42:49 +05:30
committed by GitHub
parent 0ecd2b1b60
commit 4c0d2d366a
10 changed files with 96 additions and 63 deletions

View File

@@ -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,

View File

@@ -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(

View File

@@ -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(

View File

@@ -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"},

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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",
}

View File

@@ -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

View File

@@ -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<TMobileAuthEmailValidationForm> =
const handleFormSubmit = async (event: FormEvent<HTMLFormElement>) => {
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,
};