mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 13:29:56 +02:00
[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:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user