From 8ef859930560d31edeeb6a376999038b81a9c409 Mon Sep 17 00:00:00 2001 From: Armaan Alam Date: Sun, 19 Jan 2025 03:05:11 +0530 Subject: [PATCH] auth changes --- myapp/api.py | 8 ++- myapp/settings.py | 40 +++++++---- users/api_auth.py | 177 ++++++++++++++++++++++++++++++---------------- users/auth.py | 51 ++++++------- 4 files changed, 173 insertions(+), 103 deletions(-) diff --git a/myapp/api.py b/myapp/api.py index 3f4418c..524e872 100644 --- a/myapp/api.py +++ b/myapp/api.py @@ -26,9 +26,15 @@ @api.exception_handler(AuthenticationError) def custom_authentication_error_handler(request, exc): + if "Token is invalid or expired" in str(exc): + return api.create_response( + request, + {"message": "Your session has expired. Please log in again."}, + status=401, + ) return api.create_response( request, - {"message": "Please log in to access this resource."}, + {"message": "Authentication failed. Please log in again."}, status=401, ) diff --git a/myapp/settings.py b/myapp/settings.py index 1f89c15..13226ae 100644 --- a/myapp/settings.py +++ b/myapp/settings.py @@ -49,6 +49,7 @@ "articles", "posts", "storages", + "rest_framework_simplejwt.token_blacklist", ] AUTH_USER_MODEL = "users.User" @@ -60,10 +61,12 @@ } SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=180), + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15), "REFRESH_TOKEN_LIFETIME": timedelta(days=1), "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True, + "ALGORITHM": "HS256", + "SIGNING_KEY": config("JWT_SECRET_KEY", default=SECRET_KEY), } MIDDLEWARE = [ @@ -82,20 +85,29 @@ CORS_ALLOW_ALL_ORIGINS = True -# # Allow headers -# CORS_ALLOW_HEADERS = [ -# "accept", -# "accept-encoding", -# "Authorization", -# "content-type", -# "dnt", -# "origin", -# "user-agent", -# "x-csrftoken", -# "x-requested-with", -# "set-cookie", +# CORS_ALLOW_ORIGINS = [ +# "http://localhost:3000", # ] + +CORS_ALLOW_CREDENTIALS = True + +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] + +CORS_EXPOSE_HEADERS = ['content-type', 'set-cookie'] + +COOKIE_DOMAIN = config("COOKIE_DOMAIN", default="localhost") + ROOT_URLCONF = "myapp.urls" TEMPLATES = [ @@ -132,7 +144,6 @@ } if not DEBUG: - print(config("DATABASE_URL")) DATABASES["default"] = dj_database_url.parse(config("DATABASE_URL")) @@ -219,7 +230,6 @@ # Celery Configurations CELERY_BROKER_URL = config("CELERY_BROKER_URL", default="redis://localhost:6379/0") CELERY_RESULT_BACKEND = config("CELERY_RESULT_BACKEND", default="redis://localhost:6379/0") -print("redis: ", CELERY_BROKER_URL, CELERY_RESULT_BACKEND) CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' diff --git a/users/api_auth.py b/users/api_auth.py index 3fe7bdd..8827ca2 100644 --- a/users/api_auth.py +++ b/users/api_auth.py @@ -2,21 +2,23 @@ This file contains the API endpoints related to user management. """ +from datetime import datetime + from django.conf import settings from django.contrib.auth import authenticate, login from django.core.signing import BadSignature, SignatureExpired, TimestampSigner from django.db import IntegrityError from django.http import JsonResponse -from django.template.loader import render_to_string from django.utils.encoding import force_bytes -from django.utils.html import strip_tags from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from ninja import Router from ninja.errors import HttpError, HttpRequest from ninja.responses import codes_4xx +from rest_framework_simplejwt.exceptions import InvalidToken, TokenError from rest_framework_simplejwt.tokens import RefreshToken from myapp.schemas import Message +from myapp.services.send_emails import send_email_task from users.models import Reputation, User from users.schemas import ( LogInSchemaIn, @@ -25,11 +27,20 @@ UserCreateSchema, ) -from myapp.services.send_emails import send_email_task - router = Router(tags=["Users Auth"]) signer = TimestampSigner() +refresh_expiry = settings.SIMPLE_JWT["REFRESH_TOKEN_LIFETIME"].total_seconds() +debug = settings.DEBUG + +cookieSettings = { + "httponly": True, + "secure": not debug, + "samesite": 'lax', + "domain": settings.COOKIE_DOMAIN, + "path": '/', + "max_age": refresh_expiry, +} @router.post("/signup", response={201: Message, 400: Message}) def signup(request: HttpRequest, payload: UserCreateSchema): @@ -41,8 +52,7 @@ def signup(request: HttpRequest, payload: UserCreateSchema): if user: if not user.is_active: return 400, { - "message": "Email already registered but not activated. Please" - " check your email for the activation link." + "message": "Email already registered but not activated. Please check your email for the activation link." } else: return 400, {"message": "Email is already in use."} @@ -69,15 +79,15 @@ def signup(request: HttpRequest, payload: UserCreateSchema): # Render the HTML template with context html_content = { - "name": user.first_name, - "activation_link": link, - } + "name": user.first_name, + "activation_link": link, + } send_email_task.delay( - subject = "Activate your account", - html_template_name = "activation_email.html", - context = html_content, - recipient_list = [payload.email] + subject="Activate your account", + html_template_name="activation_email.html", + context=html_content, + recipient_list=[payload.email] ) except IntegrityError: @@ -85,12 +95,10 @@ def signup(request: HttpRequest, payload: UserCreateSchema): return 201, { "message": ( - "Account created successfully. Please check your " - "email to activate your account." + "Account created successfully. Please check your email to activate your account." ) } - @router.post("/activate/{token}", response={200: Message, 400: Message, 404: Message}) def activate(request: HttpRequest, token: str): try: @@ -105,7 +113,6 @@ def activate(request: HttpRequest, token: str): Reputation.objects.get_or_create(user=user) user.is_active = True - user.save() return 200, {"message": "Account activated successfully."} @@ -116,35 +123,28 @@ def activate(request: HttpRequest, token: str): except BadSignature: return 400, {"message": "Invalid activation link."} - -@router.post( - "/resend-activation/{email}", response={200: Message, 400: Message, 404: Message} -) +@router.post("/resend-activation/{email}", response={200: Message, 400: Message, 404: Message}) def resend_activation(request: HttpRequest, email: str): try: user = User.objects.get(email=email) if user.is_active: - return 400, { - "message": "This account is already active. Consider logging in." - } + return 400, {"message": "This account is already active. Consider logging in."} # Generate a new and unique activation token token = signer.sign(user.pk) link = f"{settings.FRONTEND_URL}/auth/activate/{token}" - # Render the HTML template with context html_content = { - "name": user.first_name, - "activation_link": link, - } + "name": user.first_name, + "activation_link": link, + } - # Send email send_email_task.delay( - subject = "Activate your account", - html_template_name = "resend_activation_email.html", - context = html_content, - recipient_list = [email] + subject="Activate your account", + html_template_name="resend_activation_email.html", + context=html_content, + recipient_list=[email] ) except User.DoesNotExist: @@ -152,7 +152,6 @@ def resend_activation(request: HttpRequest, email: str): return 200, {"message": "Activation link sent. Please check your email."} - @router.post("/login", response={200: LogInSchemaOut, codes_4xx: Message}) def login_user(request, payload: LogInSchemaIn): # Attempt to retrieve user by username or email @@ -163,11 +162,8 @@ def login_user(request, payload: LogInSchemaIn): if not user: return 404, {"message": "No account found with the provided username/email."} - # Check if the user's account is active if not user.is_active: - return 403, { - "message": "This account is inactive. Please activate your account." - } + return 403, {"message": "This account is inactive. Please activate your account."} # Authenticate the user user = authenticate(username=user.username, password=payload.password) @@ -178,37 +174,76 @@ def login_user(request, payload: LogInSchemaIn): # Generate JWT tokens refresh = RefreshToken.for_user(user) - access_token = str(refresh.access_token) refresh_token = str(refresh) + access_expiry = int((datetime.now() + settings.SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"]).timestamp() * 1000) + response = JsonResponse( { "status": "success", "message": "Login successful.", "token": access_token, + "expiresAt": access_expiry, + "cookie": cookieSettings } ) - response.set_cookie( - key="accessToken", - value=access_token, - httponly=True, - secure=False, # Ensure secure is False for localhost testing - samesite="None", # Adjust for cross-site requests - max_age=120 * 60, # 2 hours - ) response.set_cookie( key="refreshToken", value=refresh_token, - httponly=True, - secure=False, # Ensure secure is False for localhost testing - samesite="None", # Adjust for cross-site requests - max_age=7 * 24 * 60 * 60, # 7 days + **cookieSettings ) return response +@router.post("/refresh", response={200: Message, 401: Message}) +def refresh_token(request: HttpRequest): + try: + refresh_token = request.COOKIES.get("refreshToken") + if not refresh_token: + return 401, {"message": "Refresh token is missing."} + + refresh = RefreshToken(refresh_token) + access_token = str(refresh.access_token) + new_refresh_token = str(refresh) + access_expiry = int((datetime.now() + settings.SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"]).timestamp() * 1000) + + + # Invalidate the old token if BLACKLIST_AFTER_ROTATION is True + # if settings.SIMPLE_JWT["BLACKLIST_AFTER_ROTATION"]: + # try: + # refresh.blacklist() + # except Exception as e: + # print(f"Error blacklisting refresh token: {str(e)}") + + response = JsonResponse( + { + "status": "success", + "message": "Token refreshed successfully.", + "token": access_token, + "expiresAt": access_expiry, + "cookie": cookieSettings + } + ) + + response.set_cookie( + key="refreshToken", + value=new_refresh_token, + **cookieSettings + ) + + return response + + except TokenError as e: + print('TokenError', e) + return 401, {"message": f"Token error: {str(e)}"} + except InvalidToken: + print('TokenError', InvalidToken) + return 401, {"message": "Invalid refresh token."} + except Exception as e: + print('TokenError', e) + return 401, {"message": "Invalid refresh token."} @router.post("/forgot-password/{email}", response={200: Message, 404: Message}) def request_reset(request: HttpRequest, email: str): @@ -221,25 +256,23 @@ def request_reset(request: HttpRequest, email: str): uid = urlsafe_base64_encode(force_bytes(user.pk)) signed_uid = signer.sign(uid) - # Generate the reset link + # Generate a reset link reset_link = f"{settings.FRONTEND_URL}/auth/resetpassword/{signed_uid}" - # Render the HTML template with context html_content = { - "name": user.first_name, - "reset_link": reset_link, - } - + "name": user.first_name, + "reset_link": reset_link, + } + send_email_task.delay( - subject = "Password Reset Request", - html_template_name = "password_reset_email.html", - context = html_content, - recipient_list = [user.email] + subject="Password Reset Request", + html_template_name="password_reset_email.html", + context=html_content, + recipient_list=[user.email] ) return 200, {"message": "Password reset link has been sent to your email."} - @router.post("/reset-password/{token}", response={200: Message, 400: Message}) def reset_password(request: HttpRequest, token: str, payload: ResetPasswordSchema): try: @@ -263,3 +296,23 @@ def reset_password(request: HttpRequest, token: str, payload: ResetPasswordSchem except BadSignature: return 400, {"message": "Invalid password reset link."} + +@router.get("/logout", response={200: Message}) +def logout(request: HttpRequest): + refresh_token = request.COOKIES.get("refreshToken") + if refresh_token: + if settings.SIMPLE_JWT["BLACKLIST_AFTER_ROTATION"]: + try: + refresh = RefreshToken(refresh_token) + refresh.blacklist() + except Exception as e: + print(f"Error blacklisting refresh token: {str(e)}") + + response = JsonResponse( + { + "status": "success", + "message": "Logout successful." + } + ) + + return response diff --git a/users/auth.py b/users/auth.py index 08cfd04..3d71cef 100644 --- a/users/auth.py +++ b/users/auth.py @@ -7,8 +7,8 @@ class JWTAuth(HttpBearer): def authenticate(self, request: HttpRequest, token): - jwt_authentication = JWTAuthentication() try: + jwt_authentication = JWTAuthentication() validated_token = jwt_authentication.get_validated_token(token) user = jwt_authentication.get_user(validated_token) if user is None: @@ -24,30 +24,31 @@ def authenticate(self, request: HttpRequest, token): # Function-based view to handle partially protected endpoints def OptionalJWTAuth(request: HttpRequest): - auth_header = request.headers.get("Authorization") - if not auth_header: - return True # No token provided, proceed without authentication - if not auth_header.startswith("Bearer "): - return True # Token not in correct format, proceed without authentication + return True + # auth_header = request.headers.get("Authorization") + # if not auth_header: + # return True # No token provided, proceed without authentication + # if not auth_header.startswith("Bearer "): + # return True # Token not in correct format, proceed without authentication - token = auth_header.split("Bearer ")[1] + # token = auth_header.split("Bearer ")[1] - # If token is null, return True to proceed without authentication - # Note: token is string null that is set in the frontend when user is not logged in - if token is None or token == "null": - return True + # # If token is null, return True to proceed without authentication + # # Note: token is string null that is set in the frontend when user is not logged in + # if token is None or token == "null": + # return True - jwt_authentication = JWTAuthentication() - try: - validated_token = jwt_authentication.get_validated_token(token) - user = jwt_authentication.get_user(validated_token) - if user is None: - raise HttpError(403, "Authentication failed: Unable to identify user.") - request.auth = user - return user - except TokenError as e: - raise HttpError(401, f"Token error: {str(e)}") - except InvalidToken: - raise HttpError(401, "Your session has expired. Please log in again.") - except Exception as e: - raise HttpError(500, f"Authentication failed: {str(e)}") + # jwt_authentication = JWTAuthentication() + # try: + # validated_token = jwt_authentication.get_validated_token(token) + # user = jwt_authentication.get_user(validated_token) + # if user is None: + # raise HttpError(403, "Authentication failed: Unable to identify user.") + # request.auth = user + # return user + # except TokenError as e: + # raise HttpError(401, f"Token error: {str(e)}") + # except InvalidToken: + # raise HttpError(401, "Your session has expired. Please log in again.") + # except Exception as e: + # raise HttpError(500, f"Authentication failed: {str(e)}")