From 69bb09ef34cf212104878ca936cedb3b478db8b2 Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 15:50:46 +0530 Subject: [PATCH 1/6] refactored google auth callback --- todo/tests/unit/views/test_auth.py | 44 ----- todo/urls.py | 2 - todo/views/auth.py | 270 +++++------------------------ todo/views/task.py | 2 +- 4 files changed, 45 insertions(+), 273 deletions(-) diff --git a/todo/tests/unit/views/test_auth.py b/todo/tests/unit/views/test_auth.py index b1545896..0f2fb3c6 100644 --- a/todo/tests/unit/views/test_auth.py +++ b/todo/tests/unit/views/test_auth.py @@ -131,51 +131,7 @@ def test_get_handles_callback_successfully(self, mock_create_user, mock_handle_c self.assertNotIn("oauth_state", request.session) -class GoogleAuthStatusViewTests(APISimpleTestCase): - def setUp(self): - super().setUp() - self.client = APIClient() - self.url = reverse("google_status") - - def test_get_returns_401_when_no_access_token(self): - response = self.client.get(self.url) - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertEqual(response.data["message"], AuthErrorMessages.NO_ACCESS_TOKEN) - self.assertEqual(response.data["authenticated"], False) - self.assertEqual(response.data["statusCode"], status.HTTP_401_UNAUTHORIZED) - - @patch("todo.utils.google_jwt_utils.validate_google_access_token") - @patch("todo.services.user_service.UserService.get_user_by_id") - def test_get_returns_user_info_when_authenticated(self, mock_get_user, mock_validate_token): - user_id = str(ObjectId()) - user_data = { - "user_id": user_id, - "google_id": "test_google_id", - "email": "test@example.com", - "name": "Test User", - } - mock_validate_token.return_value = user_data - - mock_user = Mock() - mock_user.id = ObjectId(user_id) - mock_user.google_id = "test_google_id" - mock_user.email_id = "test@example.com" - mock_user.name = "Test User" - type(mock_user).id = PropertyMock(return_value=ObjectId(user_id)) - mock_get_user.return_value = mock_user - - tokens = generate_google_token_pair(user_data) - self.client.cookies["ext-access"] = tokens["access_token"] - - response = self.client.get(self.url, HTTP_ACCEPT="application/json") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["data"]["user"]["id"], user_id) - self.assertEqual(response.data["data"]["user"]["email"], mock_user.email_id) - self.assertEqual(response.data["data"]["user"]["name"], mock_user.name) - self.assertEqual(response.data["data"]["user"]["google_id"], mock_user.google_id) class GoogleRefreshViewTests(APISimpleTestCase): diff --git a/todo/urls.py b/todo/urls.py index d370c6af..6827dad3 100644 --- a/todo/urls.py +++ b/todo/urls.py @@ -6,7 +6,6 @@ GoogleCallbackView, GoogleRefreshView, GoogleLogoutView, - GoogleAuthStatusView, ) urlpatterns = [ @@ -15,7 +14,6 @@ path("health", HealthView.as_view(), name="health"), path("auth/google/login/", GoogleLoginView.as_view(), name="google_login"), path("auth/google/callback/", GoogleCallbackView.as_view(), name="google_callback"), - path("auth/google/status/", GoogleAuthStatusView.as_view(), name="google_status"), path("auth/google/refresh/", GoogleRefreshView.as_view(), name="google_refresh"), path("auth/google/logout/", GoogleLogoutView.as_view(), name="google_logout"), ] diff --git a/todo/views/auth.py b/todo/views/auth.py index 40e8cc42..3df5c7fb 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -2,15 +2,14 @@ from rest_framework.response import Response from rest_framework.request import Request from rest_framework import status -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponseRedirect from django.conf import settings -from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample, OpenApiResponse +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse from drf_spectacular.types import OpenApiTypes from todo.services.google_oauth_service import GoogleOAuthService from todo.services.user_service import UserService from todo.utils.google_jwt_utils import ( validate_google_refresh_token, - validate_google_access_token, generate_google_access_token, generate_google_token_pair, ) @@ -19,7 +18,6 @@ from todo.exceptions.google_auth_exceptions import ( GoogleAuthException, GoogleTokenExpiredError, - GoogleTokenInvalidError, GoogleTokenMissingError, GoogleAPIException, ) @@ -70,15 +68,6 @@ def get(self, request: Request): class GoogleCallbackView(APIView): - """ - This class has two implementations: - 1. Current active implementation (temporary) - For testing and development - 2. Commented implementation - For frontend integration (to be used later) - - The temporary implementation processes the OAuth callback directly and shows a success page. - The frontend implementation will redirect to the frontend and process the callback via POST request. - """ - @extend_schema( operation_id="google_callback", summary="Handle Google OAuth callback", @@ -114,12 +103,33 @@ class GoogleCallbackView(APIView): }, ) def get(self, request: Request): - if "error" in request.query_params: - error = request.query_params.get("error") - raise GoogleAuthException(error) - code = request.query_params.get("code") state = request.query_params.get("state") + error = request.query_params.get("error") + + frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" + + if error: + return HttpResponseRedirect(f"{frontend_callback}?error={error}") + elif code and state: + return HttpResponseRedirect(f"{frontend_callback}?code={code}&state={state}") + else: + return HttpResponseRedirect(f"{frontend_callback}?error=missing_parameters") + + @extend_schema( + operation_id="google_callback_post", + summary="Handle Google OAuth callback (POST)", + description="Processes the OAuth callback from Google via POST request", + tags=["auth"], + responses={ + 200: OpenApiResponse(description="OAuth callback processed successfully"), + 400: OpenApiResponse(description="Bad request - invalid parameters"), + 500: OpenApiResponse(description="Internal server error"), + }, + ) + def post(self, request: Request): + code = request.data.get("code") + state = request.data.get("state") if not code: raise GoogleAuthException("No authorization code received from Google") @@ -128,9 +138,6 @@ def get(self, request: Request): if not stored_state or stored_state != state: raise GoogleAuthException("Invalid state parameter") - return self._handle_callback_directly(code, request) - - def _handle_callback_directly(self, code, request): try: google_data = GoogleOAuthService.handle_callback(code) user = UserService.create_or_update_user(google_data) @@ -144,62 +151,24 @@ def _handle_callback_directly(self, code, request): } ) - wants_json = ( - "application/json" in request.headers.get("Accept", "").lower() - or request.query_params.get("format") == "json" - ) - - if wants_json: - response = Response( - { - "statusCode": status.HTTP_200_OK, - "message": AppMessages.GOOGLE_LOGIN_SUCCESS, - "data": { - "user": { - "id": str(user.id), - "name": user.name, - "email": user.email_id, - "google_id": user.google_id, - }, - "tokens": { - "access_token_expires_in": tokens["expires_in"], - "refresh_token_expires_in": settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], - }, + response = Response( + { + "statusCode": status.HTTP_200_OK, + "message": AppMessages.GOOGLE_LOGIN_SUCCESS, + "data": { + "user": { + "id": str(user.id), + "name": user.name, + "email": user.email_id, + "google_id": user.google_id, + }, + "tokens": { + "access_token_expires_in": tokens["expires_in"], + "refresh_token_expires_in": settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], }, - } - ) - else: - response = HttpResponse(f""" - - โœ… Login Successful - -

โœ… Google OAuth Login Successful!

- -

๐Ÿง‘โ€๐Ÿ’ป User Info:

- - -

๐Ÿช Authentication Cookies Set:

- - -

๐Ÿงช Test Other Endpoints:

- - -

Google OAuth integration is working perfectly!

- - - """) + }, + } + ) self._set_auth_cookies(response, tokens) request.session.pop("oauth_state", None) @@ -225,157 +194,6 @@ def _set_auth_cookies(self, response, tokens): ) -# Frontend integration implementation (to be used later) -""" -class GoogleCallbackViewFrontend(APIView): - def get(self, request: Request): - code = request.query_params.get("code") - state = request.query_params.get("state") - error = request.query_params.get("error") - - frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" - - if error: - return HttpResponseRedirect(f"{frontend_callback}?error={error}") - elif code and state: - return HttpResponseRedirect(f"{frontend_callback}?code={code}&state={state}") - else: - return HttpResponseRedirect(f"{frontend_callback}?error=missing_parameters") - - def post(self, request: Request): - code = request.data.get("code") - state = request.data.get("state") - - if not code: - formatted_errors = [ - ApiErrorDetail( - source={ApiErrorSource.PARAMETER: "code"}, - title=ApiErrors.VALIDATION_ERROR, - detail=ApiErrors.INVALID_AUTH_CODE, - ) - ] - error_response = ApiErrorResponse( - statusCode=400, - message=ApiErrors.INVALID_AUTH_CODE, - errors=formatted_errors - ) - return Response( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_400_BAD_REQUEST - ) - - stored_state = request.session.get("oauth_state") - if not stored_state or stored_state != state: - formatted_errors = [ - ApiErrorDetail( - source={ApiErrorSource.PARAMETER: "state"}, - title=ApiErrors.VALIDATION_ERROR, - detail=ApiErrors.INVALID_STATE_PARAMETER, - ) - ] - error_response = ApiErrorResponse( - statusCode=400, - message=ApiErrors.INVALID_STATE_PARAMETER, - errors=formatted_errors - ) - return Response( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_400_BAD_REQUEST - ) - - google_data = GoogleOAuthService.handle_callback(code) - user = UserService.create_or_update_user(google_data) - - tokens = generate_google_token_pair( - { - "user_id": str(user.id), - "google_id": user.google_id, - "email": user.email_id, - "name": user.name, - } - ) - - response = Response({ - "statusCode": status.HTTP_200_OK, - "message": AppMessages.GOOGLE_LOGIN_SUCCESS, - "data": { - "user": { - "id": str(user.id), - "name": user.name, - "email": user.email_id, - "google_id": user.google_id, - }, - "tokens": { - "access_token_expires_in": tokens["expires_in"], - "refresh_token_expires_in": settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"] - } - } - }) - - self._set_auth_cookies(response, tokens) - request.session.pop("oauth_state", None) - - return response - - def _get_cookie_config(self): - return { - "path": "/", - "domain": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_DOMAIN"), - "secure": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SECURE", False), - "httponly": True, - "samesite": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SAMESITE", "Lax"), - } - - def _set_auth_cookies(self, response, tokens): - config = self._get_cookie_config() - response.set_cookie("ext-access", tokens["access_token"], max_age=tokens["expires_in"], **config) - response.set_cookie( - "ext-refresh", tokens["refresh_token"], max_age=settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], **config - ) -""" - - -class GoogleAuthStatusView(APIView): - @extend_schema( - operation_id="google_auth_status", - summary="Check authentication status", - description="Check if the user is authenticated and return user information", - tags=["auth"], - responses={ - 200: OpenApiResponse(description="Authentication status retrieved successfully"), - 401: OpenApiResponse(description="Unauthorized - invalid or missing token"), - 500: OpenApiResponse(description="Internal server error"), - }, - ) - def get(self, request: Request): - access_token = request.COOKIES.get("ext-access") - - if not access_token: - raise GoogleTokenMissingError(AuthErrorMessages.NO_ACCESS_TOKEN) - - try: - payload = validate_google_access_token(access_token) - user = UserService.get_user_by_id(payload["user_id"]) - except Exception as e: - raise GoogleTokenInvalidError(str(e)) - - return Response( - { - "statusCode": status.HTTP_200_OK, - "message": "Authentication status retrieved successfully", - "data": { - "authenticated": True, - "user": { - "id": str(user.id), - "email": user.email_id, - "name": user.name, - "google_id": user.google_id, - }, - }, - } - ) - - class GoogleRefreshView(APIView): @extend_schema( operation_id="google_refresh_token", diff --git a/todo/views/task.py b/todo/views/task.py index bbcaf353..4bad214a 100644 --- a/todo/views/task.py +++ b/todo/views/task.py @@ -5,7 +5,7 @@ from rest_framework.request import Request from rest_framework.exceptions import ValidationError from django.conf import settings -from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample, OpenApiResponse +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse from drf_spectacular.types import OpenApiTypes from todo.middlewares.jwt_auth import get_current_user_info from todo.serializers.get_tasks_serializer import GetTaskQueryParamsSerializer From 83f151264deb46cbd69b15410114bd337157e405 Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 16:03:41 +0530 Subject: [PATCH 2/6] fix: failing tests --- todo/tests/unit/views/test_auth.py | 70 +++++++++++++++++------------- todo/views/auth.py | 13 ++++-- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/todo/tests/unit/views/test_auth.py b/todo/tests/unit/views/test_auth.py index 0f2fb3c6..4ffe8263 100644 --- a/todo/tests/unit/views/test_auth.py +++ b/todo/tests/unit/views/test_auth.py @@ -1,4 +1,4 @@ -from rest_framework.test import APISimpleTestCase, APIClient, APIRequestFactory +from rest_framework.test import APITestCase, APIClient, APIRequestFactory from rest_framework.reverse import reverse from rest_framework import status from unittest.mock import patch, Mock, PropertyMock @@ -14,7 +14,7 @@ from todo.constants.messages import AppMessages, AuthErrorMessages -class GoogleLoginViewTests(APISimpleTestCase): +class GoogleLoginViewTests(APITestCase): def setUp(self): super().setUp() self.client = APIClient() @@ -59,7 +59,7 @@ def test_get_with_redirect_url(self, mock_get_auth_url): mock_get_auth_url.assert_called_once_with(redirect_url) -class GoogleCallbackViewTests(APISimpleTestCase): +class GoogleCallbackViewTests(APITestCase): def setUp(self): super().setUp() self.client = APIClient() @@ -67,38 +67,46 @@ def setUp(self): self.factory = APIRequestFactory() self.view = GoogleCallbackView.as_view() - def test_get_returns_error_for_oauth_error(self): + def test_get_redirects_for_oauth_error(self): error = "access_denied" - request = self.factory.get(f"{self.url}?error={error}") + response = self.client.get(f"{self.url}?error={error}") - response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertIn("error=access_denied", response.url) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data["message"], error) - self.assertEqual(response.data["errors"][0]["detail"], error) + def test_get_redirects_for_missing_code(self): + response = self.client.get(self.url) - def test_get_returns_error_for_missing_code(self): - request = self.factory.get(self.url) + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertIn("error=missing_parameters", response.url) - response = self.view(request) + def test_get_redirects_for_valid_code_and_state(self): + response = self.client.get(f"{self.url}?code=test_code&state=test_state") + + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertIn("code=test_code", response.url) + self.assertIn("state=test_state", response.url) + + def test_post_returns_error_for_missing_code(self): + response = self.client.post(self.url, {}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["message"], "No authorization code received from Google") - self.assertEqual(response.data["errors"][0]["detail"], "No authorization code received from Google") - def test_get_returns_error_for_invalid_state(self): - request = self.factory.get(f"{self.url}?code=test_code&state=invalid_state") - request.session = {"oauth_state": "different_state"} + def test_post_returns_error_for_invalid_state(self): - response = self.view(request) + session = self.client.session + session["oauth_state"] = "different_state" + session.save() + + response = self.client.post(self.url, {"code": "test_code", "state": "invalid_state"}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["message"], "Invalid state parameter") - self.assertEqual(response.data["errors"][0]["detail"], "Invalid state parameter") @patch("todo.services.google_oauth_service.GoogleOAuthService.handle_callback") @patch("todo.services.user_service.UserService.create_or_update_user") - def test_get_handles_callback_successfully(self, mock_create_user, mock_handle_callback): + def test_post_handles_callback_successfully(self, mock_create_user, mock_handle_callback): mock_google_data = { "id": "test_google_id", "email": "test@example.com", @@ -115,26 +123,26 @@ def test_get_handles_callback_successfully(self, mock_create_user, mock_handle_c mock_handle_callback.return_value = mock_google_data mock_create_user.return_value = mock_user - request = self.factory.get(f"{self.url}?code=test_code&state=test_state") - request.session = {"oauth_state": "test_state"} + session = self.client.session + session["oauth_state"] = "test_state" + session.save() - response = self.view(request) + response = self.client.post(self.url, {"code": "test_code", "state": "test_state"}) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn("โœ… Google OAuth Login Successful!", response.content.decode()) - self.assertIn(str(mock_user.id), response.content.decode()) - self.assertIn(mock_user.name, response.content.decode()) - self.assertIn(mock_user.email_id, response.content.decode()) - self.assertIn(mock_user.google_id, response.content.decode()) + self.assertEqual(response.data["data"]["user"]["id"], user_id) + self.assertEqual(response.data["data"]["user"]["name"], mock_user.name) + self.assertEqual(response.data["data"]["user"]["email"], mock_user.email_id) + self.assertEqual(response.data["data"]["user"]["google_id"], mock_user.google_id) self.assertIn("ext-access", response.cookies) self.assertIn("ext-refresh", response.cookies) - self.assertNotIn("oauth_state", request.session) + self.assertNotIn("oauth_state", self.client.session) -class GoogleRefreshViewTests(APISimpleTestCase): +class GoogleRefreshViewTests(APITestCase): def setUp(self): super().setUp() self.client = APIClient() @@ -169,7 +177,7 @@ def test_get_refreshes_token_successfully(self, mock_validate_token): self.assertIn("ext-access", response.cookies) -class GoogleLogoutViewTests(APISimpleTestCase): +class GoogleLogoutViewTests(APITestCase): def setUp(self): super().setUp() self.client = APIClient() @@ -187,7 +195,7 @@ def test_get_returns_success_and_clears_cookies(self): self.client.cookies["ext-refresh"] = tokens["refresh_token"] response = self.client.get(self.url, HTTP_ACCEPT="application/json") - + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["data"]["success"], True) self.assertEqual(response.data["message"], AppMessages.GOOGLE_LOGOUT_SUCCESS) diff --git a/todo/views/auth.py b/todo/views/auth.py index 3df5c7fb..3015c5bc 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -308,9 +308,7 @@ def _handle_logout(self, request: Request): redirect_url = redirect_url or "/" response = HttpResponseRedirect(redirect_url) - config = self._get_cookie_config() - response.delete_cookie("ext-access", **config) - response.delete_cookie("ext-refresh", **config) + self._clear_auth_cookies(response) return response @@ -322,3 +320,12 @@ def _get_cookie_config(self): "httponly": True, "samesite": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SAMESITE", "Lax"), } + + def _clear_auth_cookies(self, response): + """Clear authentication cookies with only the parameters that delete_cookie accepts""" + delete_config = { + "path": "/", + "domain": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_DOMAIN"), + } + response.delete_cookie("ext-access", **delete_config) + response.delete_cookie("ext-refresh", **delete_config) From 44b71abd8487b824e3b75abef4f9938c966f3666 Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 16:34:26 +0530 Subject: [PATCH 3/6] fix: login issue --- .env.example | 5 +++- todo/tests/unit/views/test_auth.py | 38 ++++++++++++++++++++------ todo/views/auth.py | 44 ++++++++++++++++++++++++++---- 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 99b0ef3a..74c5b2a9 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,7 @@ RDS_PUBLIC_KEY="public-key-here" GOOGLE_OAUTH_CLIENT_ID="google-client-id" GOOGLE_OAUTH_CLIENT_SECRET="client-secret" GOOGLE_OAUTH_REDIRECT_URI="environment-url/auth/google/callback" -GOOGLE_JWT_SECRET_KEY=generate-secret-key \ No newline at end of file +GOOGLE_JWT_SECRET_KEY=generate-secret-key + +GOOGLE_JWT_SECRET_KEY="7i=aje)abyu!8m3jc9&vlmo@e-o__65_jw_=g^ktw7@+saa1z(" +GOOGLE_OAUTH_REDIRECT_URI="http://localhost:8000/v1/auth/google/callback" \ No newline at end of file diff --git a/todo/tests/unit/views/test_auth.py b/todo/tests/unit/views/test_auth.py index 4ffe8263..1ae21cb0 100644 --- a/todo/tests/unit/views/test_auth.py +++ b/todo/tests/unit/views/test_auth.py @@ -78,14 +78,38 @@ def test_get_redirects_for_missing_code(self): response = self.client.get(self.url) self.assertEqual(response.status_code, status.HTTP_302_FOUND) - self.assertIn("error=missing_parameters", response.url) + self.assertIn("error=missing_code", response.url) + + @patch("todo.services.google_oauth_service.GoogleOAuthService.handle_callback") + @patch("todo.services.user_service.UserService.create_or_update_user") + def test_get_redirects_for_valid_code_and_state(self, mock_create_user, mock_handle_callback): + mock_google_data = { + "id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + user_id = str(ObjectId()) + mock_user = Mock() + mock_user.id = ObjectId(user_id) + mock_user.google_id = mock_google_data["id"] + mock_user.email_id = mock_google_data["email"] + mock_user.name = mock_google_data["name"] + type(mock_user).id = PropertyMock(return_value=ObjectId(user_id)) + + mock_handle_callback.return_value = mock_google_data + mock_create_user.return_value = mock_user + + session = self.client.session + session["oauth_state"] = "test_state" + session.save() - def test_get_redirects_for_valid_code_and_state(self): response = self.client.get(f"{self.url}?code=test_code&state=test_state") self.assertEqual(response.status_code, status.HTTP_302_FOUND) - self.assertIn("code=test_code", response.url) - self.assertIn("state=test_state", response.url) + self.assertIn("success=true", response.url) + self.assertIn("ext-access", response.cookies) + self.assertIn("ext-refresh", response.cookies) + self.assertNotIn("oauth_state", self.client.session) def test_post_returns_error_for_missing_code(self): response = self.client.post(self.url, {}) @@ -94,7 +118,6 @@ def test_post_returns_error_for_missing_code(self): self.assertEqual(response.data["message"], "No authorization code received from Google") def test_post_returns_error_for_invalid_state(self): - session = self.client.session session["oauth_state"] = "different_state" session.save() @@ -139,9 +162,6 @@ def test_post_handles_callback_successfully(self, mock_create_user, mock_handle_ self.assertNotIn("oauth_state", self.client.session) - - - class GoogleRefreshViewTests(APITestCase): def setUp(self): super().setUp() @@ -195,7 +215,7 @@ def test_get_returns_success_and_clears_cookies(self): self.client.cookies["ext-refresh"] = tokens["refresh_token"] response = self.client.get(self.url, HTTP_ACCEPT="application/json") - + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["data"]["success"], True) self.assertEqual(response.data["message"], AppMessages.GOOGLE_LOGOUT_SUCCESS) diff --git a/todo/views/auth.py b/todo/views/auth.py index 3015c5bc..2a163a3e 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -107,14 +107,46 @@ def get(self, request: Request): state = request.query_params.get("state") error = request.query_params.get("error") - frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" - if error: + frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" return HttpResponseRedirect(f"{frontend_callback}?error={error}") - elif code and state: - return HttpResponseRedirect(f"{frontend_callback}?code={code}&state={state}") - else: - return HttpResponseRedirect(f"{frontend_callback}?error=missing_parameters") + + if not code: + frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" + return HttpResponseRedirect(f"{frontend_callback}?error=missing_code") + + if not state: + frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" + return HttpResponseRedirect(f"{frontend_callback}?error=missing_state") + + stored_state = request.session.get("oauth_state") + if not stored_state or stored_state != state: + frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" + return HttpResponseRedirect(f"{frontend_callback}?error=invalid_state") + + try: + google_data = GoogleOAuthService.handle_callback(code) + user = UserService.create_or_update_user(google_data) + + tokens = generate_google_token_pair( + { + "user_id": str(user.id), + "google_id": user.google_id, + "email": user.email_id, + "name": user.name, + } + ) + + frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" + response = HttpResponseRedirect(f"{frontend_callback}?success=true") + + self._set_auth_cookies(response, tokens) + request.session.pop("oauth_state", None) + + return response + except Exception: + frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" + return HttpResponseRedirect(f"{frontend_callback}?error=auth_failed") @extend_schema( operation_id="google_callback_post", From 5a85319342517ba25a9048b876d94ebcd324dd3e Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 16:45:37 +0530 Subject: [PATCH 4/6] removed token lifetime --- todo/views/auth.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/todo/views/auth.py b/todo/views/auth.py index 2a163a3e..56847b36 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -194,10 +194,6 @@ def post(self, request: Request): "email": user.email_id, "google_id": user.google_id, }, - "tokens": { - "access_token_expires_in": tokens["expires_in"], - "refresh_token_expires_in": settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], - }, }, } ) From 8a133033ac1baf27db7403c7825b8a8386cde88b Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 22:28:41 +0530 Subject: [PATCH 5/6] added rsa token and adjusted logic accordingly --- .env.example | 10 ++- todo/middlewares/jwt_auth.py | 111 ++++++++++++++++++----- todo/urls.py | 2 - todo/utils/google_jwt_utils.py | 41 ++++----- todo/views/auth.py | 160 ++++----------------------------- todo_project/settings/base.py | 5 +- 6 files changed, 133 insertions(+), 196 deletions(-) diff --git a/.env.example b/.env.example index 74c5b2a9..66c6fe0c 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,10 @@ RDS_BACKEND_BASE_URL='http://localhost:3000' RDS_PUBLIC_KEY="public-key-here" GOOGLE_OAUTH_CLIENT_ID="google-client-id" GOOGLE_OAUTH_CLIENT_SECRET="client-secret" -GOOGLE_OAUTH_REDIRECT_URI="environment-url/auth/google/callback" -GOOGLE_JWT_SECRET_KEY=generate-secret-key +# Google JWT RSA Keys +GOOGLE_JWT_PRIVATE_KEY="generate keys and paste here" +GOOGLE_JWT_PUBLIC_KEY="generate keys and paste here" -GOOGLE_JWT_SECRET_KEY="7i=aje)abyu!8m3jc9&vlmo@e-o__65_jw_=g^ktw7@+saa1z(" -GOOGLE_OAUTH_REDIRECT_URI="http://localhost:8000/v1/auth/google/callback" \ No newline at end of file +# use if required +# GOOGLE_JWT_ACCESS_LIFETIME="20" +# GOOGLE_JWT_REFRESH_LIFETIME="30" \ No newline at end of file diff --git a/todo/middlewares/jwt_auth.py b/todo/middlewares/jwt_auth.py index 8559d404..d5b71419 100644 --- a/todo/middlewares/jwt_auth.py +++ b/todo/middlewares/jwt_auth.py @@ -3,9 +3,17 @@ from django.http import JsonResponse from todo.utils.jwt_utils import verify_jwt_token -from todo.utils.google_jwt_utils import validate_google_access_token +from todo.utils.google_jwt_utils import ( + validate_google_access_token, + validate_google_refresh_token, + generate_google_access_token, +) from todo.exceptions.auth_exceptions import TokenMissingError, TokenExpiredError, TokenInvalidError -from todo.exceptions.google_auth_exceptions import GoogleTokenExpiredError, GoogleTokenInvalidError +from todo.exceptions.google_auth_exceptions import ( + GoogleTokenExpiredError, + GoogleTokenInvalidError, + GoogleRefreshTokenExpiredError, +) from todo.constants.messages import AuthErrorMessages, ApiErrors from todo.dto.responses.error_response import ApiErrorResponse, ApiErrorDetail @@ -25,7 +33,8 @@ def __call__(self, request): auth_success = self._try_authentication(request) if auth_success: - return self.get_response(request) + response = self.get_response(request) + return self._process_response(request, response) else: error_response = ApiErrorResponse( statusCode=status.HTTP_401_UNAUTHORIZED, @@ -38,7 +47,8 @@ def __call__(self, request): ], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), + status=status.HTTP_401_UNAUTHORIZED ) except (TokenMissingError, TokenExpiredError, TokenInvalidError) as e: @@ -57,7 +67,8 @@ def __call__(self, request): ], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), + status=status.HTTP_401_UNAUTHORIZED ) def _try_authentication(self, request) -> bool: @@ -73,25 +84,61 @@ def _try_google_auth(self, request) -> bool: try: google_token = request.COOKIES.get("ext-access") - if not google_token: - return False - - payload = validate_google_access_token(google_token) + if google_token: + try: + payload = validate_google_access_token(google_token) + self._set_google_user_data(request, payload) + return True + except (GoogleTokenExpiredError, GoogleTokenInvalidError): + pass - request.auth_type = "google" - request.user_id = payload["user_id"] - request.google_id = payload["google_id"] - request.user_email = payload["email"] - request.user_name = payload["name"] - request.user_role = "external_user" - - return True + return self._try_google_refresh(request) except (GoogleTokenExpiredError, GoogleTokenInvalidError) as e: raise e except Exception: return False + def _try_google_refresh(self, request) -> bool: + """Try to refresh Google access token""" + try: + refresh_token = request.COOKIES.get("ext-refresh") + + if not refresh_token: + return False + + payload = validate_google_refresh_token(refresh_token) + + user_data = { + "user_id": payload["user_id"], + "google_id": payload["google_id"], + "email": payload["email"], + "name": payload.get("name", ""), + } + + new_access_token = generate_google_access_token(user_data) + + self._set_google_user_data(request, payload) + + request._new_access_token = new_access_token + request._access_token_expires = settings.GOOGLE_JWT["ACCESS_TOKEN_LIFETIME"] + + return True + + except (GoogleRefreshTokenExpiredError, GoogleTokenInvalidError): + return False + except Exception: + return False + + def _set_google_user_data(self, request, payload): + """Set Google user data on request""" + request.auth_type = "google" + request.user_id = payload["user_id"] + request.google_id = payload["google_id"] + request.user_email = payload["email"] + request.user_name = payload.get("name", "") + request.user_role = "external_user" + def _try_rds_auth(self, request) -> bool: try: rds_token = request.COOKIES.get(self.rds_cookie_name) @@ -112,6 +159,28 @@ def _try_rds_auth(self, request) -> bool: except Exception: return False + def _process_response(self, request, response): + """Process response and set new cookies if Google token was refreshed""" + if hasattr(request, '_new_access_token'): + config = self._get_cookie_config() + response.set_cookie( + "ext-access", + request._new_access_token, + max_age=request._access_token_expires, + **config + ) + return response + + def _get_cookie_config(self): + """Get Google cookie configuration""" + return { + "path": "/", + "domain": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_DOMAIN"), + "secure": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SECURE", False), + "httponly": True, + "samesite": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SAMESITE", "Lax"), + } + def _is_public_path(self, path: str) -> bool: return any(path.startswith(public_path) for public_path in settings.PUBLIC_PATHS) @@ -122,7 +191,8 @@ def _handle_rds_auth_error(self, exception): errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), + status=status.HTTP_401_UNAUTHORIZED ) def _handle_google_auth_error(self, exception): @@ -132,7 +202,8 @@ def _handle_google_auth_error(self, exception): errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), + status=status.HTTP_401_UNAUTHORIZED ) @@ -169,4 +240,4 @@ def get_current_user_info(request) -> dict: } ) - return user_info + return user_info \ No newline at end of file diff --git a/todo/urls.py b/todo/urls.py index a0540986..a098a5b2 100644 --- a/todo/urls.py +++ b/todo/urls.py @@ -5,7 +5,6 @@ from todo.views.auth import ( GoogleLoginView, GoogleCallbackView, - GoogleRefreshView, GoogleLogoutView, ) @@ -16,6 +15,5 @@ path("labels", LabelListView.as_view(), name="labels"), path("auth/google/login/", GoogleLoginView.as_view(), name="google_login"), path("auth/google/callback/", GoogleCallbackView.as_view(), name="google_callback"), - path("auth/google/refresh/", GoogleRefreshView.as_view(), name="google_refresh"), path("auth/google/logout/", GoogleLogoutView.as_view(), name="google_logout"), ] diff --git a/todo/utils/google_jwt_utils.py b/todo/utils/google_jwt_utils.py index 008ba6bf..08c54519 100644 --- a/todo/utils/google_jwt_utils.py +++ b/todo/utils/google_jwt_utils.py @@ -8,7 +8,7 @@ GoogleRefreshTokenExpiredError, ) -from todo.constants.messages import AuthErrorMessages, ApiErrors +from todo.constants.messages import AuthErrorMessages def generate_google_access_token(user_data: dict) -> str: @@ -28,14 +28,11 @@ def generate_google_access_token(user_data: dict) -> str: "token_type": "access", } - token = jwt.encode( - payload=payload, key=settings.GOOGLE_JWT["SECRET_KEY"], algorithm=settings.GOOGLE_JWT["ALGORITHM"] - ) - + token = jwt.encode(payload=payload, key=settings.GOOGLE_JWT["PRIVATE_KEY"], algorithm=settings.GOOGLE_JWT["ALGORITHM"]) return token - except Exception: - raise GoogleTokenInvalidError(ApiErrors.GOOGLE_API_ERROR) + except Exception as e: + raise GoogleTokenInvalidError(f"Token generation failed: {str(e)}") def generate_google_refresh_token(user_data: dict) -> str: @@ -53,22 +50,17 @@ def generate_google_refresh_token(user_data: dict) -> str: "email": user_data["email"], "token_type": "refresh", } - - token = jwt.encode( - payload=payload, key=settings.GOOGLE_JWT["SECRET_KEY"], algorithm=settings.GOOGLE_JWT["ALGORITHM"] - ) + token = jwt.encode(payload=payload, key=settings.GOOGLE_JWT["PRIVATE_KEY"], algorithm=settings.GOOGLE_JWT["ALGORITHM"]) return token - except Exception: - raise GoogleTokenInvalidError(ApiErrors.GOOGLE_API_ERROR) + except Exception as e: + raise GoogleTokenInvalidError(f"Refresh token generation failed: {str(e)}") def validate_google_access_token(token: str) -> dict: try: - payload = jwt.decode( - jwt=token, key=settings.GOOGLE_JWT["SECRET_KEY"], algorithms=[settings.GOOGLE_JWT["ALGORITHM"]] - ) + payload = jwt.decode(jwt=token, key=settings.GOOGLE_JWT["PUBLIC_KEY"], algorithms=[settings.GOOGLE_JWT["ALGORITHM"]]) if payload.get("token_type") != "access": raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) @@ -77,16 +69,15 @@ def validate_google_access_token(token: str) -> dict: except jwt.ExpiredSignatureError: raise GoogleTokenExpiredError() - except jwt.InvalidTokenError: - raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) + except jwt.InvalidTokenError as e: + raise GoogleTokenInvalidError(f"Invalid token: {str(e)}") + except Exception as e: + raise GoogleTokenInvalidError(f"Token validation failed: {str(e)}") def validate_google_refresh_token(token: str) -> dict: try: - payload = jwt.decode( - jwt=token, key=settings.GOOGLE_JWT["SECRET_KEY"], algorithms=[settings.GOOGLE_JWT["ALGORITHM"]] - ) - + payload = jwt.decode(jwt=token, key=settings.GOOGLE_JWT["PUBLIC_KEY"], algorithms=[settings.GOOGLE_JWT["ALGORITHM"]]) if payload.get("token_type") != "refresh": raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) @@ -94,8 +85,10 @@ def validate_google_refresh_token(token: str) -> dict: except jwt.ExpiredSignatureError: raise GoogleRefreshTokenExpiredError() - except jwt.InvalidTokenError: - raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) + except jwt.InvalidTokenError as e: + raise GoogleTokenInvalidError(f"Invalid refresh token: {str(e)}") + except Exception as e: + raise GoogleTokenInvalidError(f"Refresh token validation failed: {str(e)}") def generate_google_token_pair(user_data: dict) -> dict: diff --git a/todo/views/auth.py b/todo/views/auth.py index 04bf7f46..adf81fa7 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -8,20 +8,8 @@ from drf_spectacular.types import OpenApiTypes from todo.services.google_oauth_service import GoogleOAuthService from todo.services.user_service import UserService -from todo.utils.google_jwt_utils import ( - validate_google_refresh_token, - generate_google_access_token, - generate_google_token_pair, -) - -from todo.constants.messages import AuthErrorMessages, AppMessages -from todo.exceptions.google_auth_exceptions import ( - GoogleAuthException, - GoogleTokenExpiredError, - GoogleTokenMissingError, - GoogleAPIException, -) - +from todo.utils.google_jwt_utils import generate_google_token_pair +from todo.constants.messages import AppMessages class GoogleLoginView(APIView): @extend_schema( @@ -149,62 +137,6 @@ def get(self, request: Request): frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" return HttpResponseRedirect(f"{frontend_callback}?error=auth_failed") - @extend_schema( - operation_id="google_callback_post", - summary="Handle Google OAuth callback (POST)", - description="Processes the OAuth callback from Google via POST request", - tags=["auth"], - responses={ - 200: OpenApiResponse(description="OAuth callback processed successfully"), - 400: OpenApiResponse(description="Bad request - invalid parameters"), - 500: OpenApiResponse(description="Internal server error"), - }, - ) - def post(self, request: Request): - code = request.data.get("code") - state = request.data.get("state") - - if not code: - raise GoogleAuthException("No authorization code received from Google") - - stored_state = request.session.get("oauth_state") - if not stored_state or stored_state != state: - raise GoogleAuthException("Invalid state parameter") - - try: - google_data = GoogleOAuthService.handle_callback(code) - user = UserService.create_or_update_user(google_data) - - tokens = generate_google_token_pair( - { - "user_id": str(user.id), - "google_id": user.google_id, - "email": user.email_id, - "name": user.name, - } - ) - - response = Response( - { - "statusCode": status.HTTP_200_OK, - "message": AppMessages.GOOGLE_LOGIN_SUCCESS, - "data": { - "user": { - "id": str(user.id), - "name": user.name, - "email": user.email_id, - "google_id": user.google_id, - }, - }, - } - ) - - self._set_auth_cookies(response, tokens) - request.session.pop("oauth_state", None) - - return response - except Exception as e: - raise GoogleAPIException(str(e)) def _get_cookie_config(self): return { @@ -222,58 +154,6 @@ def _set_auth_cookies(self, response, tokens): "ext-refresh", tokens["refresh_token"], max_age=settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], **config ) - -class GoogleRefreshView(APIView): - @extend_schema( - operation_id="google_refresh_token", - summary="Refresh access token", - description="Refresh the access token using the refresh token from cookies", - tags=["auth"], - responses={ - 200: OpenApiResponse(description="Token refreshed successfully"), - 401: OpenApiResponse(description="Unauthorized - invalid or missing refresh token"), - 500: OpenApiResponse(description="Internal server error"), - }, - ) - def get(self, request: Request): - refresh_token = request.COOKIES.get("ext-refresh") - - if not refresh_token: - raise GoogleTokenMissingError(AuthErrorMessages.NO_REFRESH_TOKEN) - - try: - payload = validate_google_refresh_token(refresh_token) - user_data = { - "user_id": payload["user_id"], - "google_id": payload["google_id"], - "email": payload["email"], - "name": payload.get("name", ""), - } - new_access_token = generate_google_access_token(user_data) - - response = Response( - {"statusCode": status.HTTP_200_OK, "message": AppMessages.TOKEN_REFRESHED, "data": {"success": True}} - ) - - config = self._get_cookie_config() - response.set_cookie( - "ext-access", new_access_token, max_age=settings.GOOGLE_JWT["ACCESS_TOKEN_LIFETIME"], **config - ) - - return response - except Exception as e: - raise GoogleTokenExpiredError(str(e)) - - def _get_cookie_config(self): - return { - "path": "/", - "domain": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_DOMAIN"), - "secure": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SECURE", False), - "httponly": True, - "samesite": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SAMESITE", "Lax"), - } - - class GoogleLogoutView(APIView): @extend_schema( operation_id="google_logout", @@ -317,28 +197,15 @@ def post(self, request: Request): return self._handle_logout(request) def _handle_logout(self, request: Request): - redirect_url = request.query_params.get("redirectURL") - - wants_json = ( - "application/json" in request.headers.get("Accept", "").lower() - or request.query_params.get("format") == "json" - or request.method == "POST" - ) - - if wants_json: - response = Response( - { - "statusCode": status.HTTP_200_OK, - "message": AppMessages.GOOGLE_LOGOUT_SUCCESS, - "data": {"success": True}, - } - ) - else: - redirect_url = redirect_url or "/" - response = HttpResponseRedirect(redirect_url) - + request.session.flush() + + response = Response({ + "statusCode": status.HTTP_200_OK, + "message": AppMessages.GOOGLE_LOGOUT_SUCCESS, + "data": {"success": True}, + }) + self._clear_auth_cookies(response) - return response def _get_cookie_config(self): @@ -351,10 +218,15 @@ def _get_cookie_config(self): } def _clear_auth_cookies(self, response): - """Clear authentication cookies with only the parameters that delete_cookie accepts""" delete_config = { "path": "/", "domain": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_DOMAIN"), } response.delete_cookie("ext-access", **delete_config) response.delete_cookie("ext-refresh", **delete_config) + + session_delete_config = { + "path": getattr(settings, 'SESSION_COOKIE_PATH', '/'), + "domain": getattr(settings, 'SESSION_COOKIE_DOMAIN', None), + } + response.delete_cookie("sessionid", **session_delete_config) diff --git a/todo_project/settings/base.py b/todo_project/settings/base.py index f01dbf1c..83877df3 100644 --- a/todo_project/settings/base.py +++ b/todo_project/settings/base.py @@ -120,8 +120,9 @@ } GOOGLE_JWT = { - "ALGORITHM": "HS256", - "SECRET_KEY": os.getenv("GOOGLE_JWT_SECRET_KEY"), + "ALGORITHM": "RS256", + "PRIVATE_KEY": os.getenv("GOOGLE_JWT_PRIVATE_KEY"), + "PUBLIC_KEY": os.getenv("GOOGLE_JWT_PUBLIC_KEY"), "ACCESS_TOKEN_LIFETIME": int(os.getenv("GOOGLE_JWT_ACCESS_LIFETIME", "3600")), "REFRESH_TOKEN_LIFETIME": int(os.getenv("GOOGLE_JWT_REFRESH_LIFETIME", "604800")), } From c909072af6752c796d513197d5c61e5ca48bd0ce Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Tue, 8 Jul 2025 01:55:33 +0530 Subject: [PATCH 6/6] Fix: updated tests for update auth logic (#105) * fix: tests based on updated implementation * fix: lint and format * removed commented lines * changed frontend port number to 3000 --- .github/workflows/test.yml | 2 - todo/middlewares/jwt_auth.py | 35 ++- todo/tests/integration/base_mongo_test.py | 3 +- todo/tests/unit/middlewares/test_jwt_auth.py | 4 - todo/tests/unit/views/test_auth.py | 231 +++++++++---------- todo/utils/google_jwt_utils.py | 16 +- todo/views/auth.py | 25 +- todo_project/settings/base.py | 27 ++- todo_project/settings/development.py | 4 +- 9 files changed, 176 insertions(+), 171 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f8144b9..c528d54a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,12 +12,10 @@ jobs: env: MONGODB_URI: mongodb://db:27017 DB_NAME: todo-app - GOOGLE_JWT_SECRET_KEY: "test-secret-key-for-jwt" GOOGLE_JWT_ACCESS_LIFETIME: "3600" GOOGLE_JWT_REFRESH_LIFETIME: "604800" GOOGLE_OAUTH_CLIENT_ID: "test-client-id" GOOGLE_OAUTH_CLIENT_SECRET: "test-client-secret" - GOOGLE_OAUTH_REDIRECT_URI: "http://localhost:3000/auth/callback" COOKIE_SECURE: "False" COOKIE_SAMESITE: "Lax" diff --git a/todo/middlewares/jwt_auth.py b/todo/middlewares/jwt_auth.py index d5b71419..f88e0672 100644 --- a/todo/middlewares/jwt_auth.py +++ b/todo/middlewares/jwt_auth.py @@ -47,8 +47,7 @@ def __call__(self, request): ], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) except (TokenMissingError, TokenExpiredError, TokenInvalidError) as e: @@ -67,8 +66,7 @@ def __call__(self, request): ], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) def _try_authentication(self, request) -> bool: @@ -103,28 +101,28 @@ def _try_google_refresh(self, request) -> bool: """Try to refresh Google access token""" try: refresh_token = request.COOKIES.get("ext-refresh") - + if not refresh_token: return False - + payload = validate_google_refresh_token(refresh_token) - + user_data = { "user_id": payload["user_id"], "google_id": payload["google_id"], "email": payload["email"], "name": payload.get("name", ""), } - + new_access_token = generate_google_access_token(user_data) - + self._set_google_user_data(request, payload) request._new_access_token = new_access_token request._access_token_expires = settings.GOOGLE_JWT["ACCESS_TOKEN_LIFETIME"] - + return True - + except (GoogleRefreshTokenExpiredError, GoogleTokenInvalidError): return False except Exception: @@ -161,13 +159,10 @@ def _try_rds_auth(self, request) -> bool: def _process_response(self, request, response): """Process response and set new cookies if Google token was refreshed""" - if hasattr(request, '_new_access_token'): + if hasattr(request, "_new_access_token"): config = self._get_cookie_config() response.set_cookie( - "ext-access", - request._new_access_token, - max_age=request._access_token_expires, - **config + "ext-access", request._new_access_token, max_age=request._access_token_expires, **config ) return response @@ -191,8 +186,7 @@ def _handle_rds_auth_error(self, exception): errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) def _handle_google_auth_error(self, exception): @@ -202,8 +196,7 @@ def _handle_google_auth_error(self, exception): errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) @@ -240,4 +233,4 @@ def get_current_user_info(request) -> dict: } ) - return user_info \ No newline at end of file + return user_info diff --git a/todo/tests/integration/base_mongo_test.py b/todo/tests/integration/base_mongo_test.py index f8695cc5..319dc140 100644 --- a/todo/tests/integration/base_mongo_test.py +++ b/todo/tests/integration/base_mongo_test.py @@ -20,8 +20,7 @@ def setUpClass(cls): cls.db = cls.mongo_client.get_database("testdb") cls.override = override_settings( - MONGODB_URI=cls.mongo_url, - DB_NAME="testdb", + MONGODB_URI=cls.mongo_url, DB_NAME="testdb", FRONTEND_URL="http://localhost:4000" ) cls.override.enable() DatabaseManager.reset() diff --git a/todo/tests/unit/middlewares/test_jwt_auth.py b/todo/tests/unit/middlewares/test_jwt_auth.py index 2681898c..5e10e69a 100644 --- a/todo/tests/unit/middlewares/test_jwt_auth.py +++ b/todo/tests/unit/middlewares/test_jwt_auth.py @@ -1,7 +1,6 @@ from unittest import TestCase from unittest.mock import Mock, patch from django.http import HttpRequest, JsonResponse -from django.conf import settings from rest_framework import status import json @@ -17,9 +16,6 @@ def setUp(self): self.request.path = "/v1/tasks" self.request.headers = {} self.request.COOKIES = {} - self._original_public_paths = settings.PUBLIC_PATHS - settings.PUBLIC_PATHS = ["/v1/auth/google/login"] - self.addCleanup(setattr, settings, "PUBLIC_PATHS", self._original_public_paths) def test_public_path_authentication_bypass(self): """Test that requests to public paths bypass authentication""" diff --git a/todo/tests/unit/views/test_auth.py b/todo/tests/unit/views/test_auth.py index e5232235..596fd371 100644 --- a/todo/tests/unit/views/test_auth.py +++ b/todo/tests/unit/views/test_auth.py @@ -4,14 +4,10 @@ from unittest.mock import patch, Mock, PropertyMock from bson.objectid import ObjectId -from todo.views.auth import ( - GoogleCallbackView, -) - -from todo.utils.google_jwt_utils import ( - generate_google_token_pair, -) -from todo.constants.messages import AppMessages, AuthErrorMessages +from todo.views.auth import GoogleCallbackView +from todo.utils.google_jwt_utils import generate_google_token_pair +from todo.constants.messages import AppMessages +from todo.tests.fixtures.user import google_auth_user_payload, users_db_data class GoogleLoginViewTests(APITestCase): @@ -21,7 +17,7 @@ def setUp(self): self.url = reverse("google_login") @patch("todo.services.google_oauth_service.GoogleOAuthService.get_authorization_url") - def test_get_returns_redirect_url_for_html_request(self, mock_get_auth_url): + def test_get_returns_redirect_for_html_request(self, mock_get_auth_url): mock_auth_url = "https://accounts.google.com/o/oauth2/auth" mock_state = "test_state" mock_get_auth_url.return_value = (mock_auth_url, mock_state) @@ -41,12 +37,31 @@ def test_get_returns_json_for_json_request(self, mock_get_auth_url): response = self.client.get(self.url, HTTP_ACCEPT="application/json") self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["statusCode"], status.HTTP_200_OK) + self.assertEqual(response.data["message"], "Google OAuth URL generated successfully") self.assertEqual(response.data["data"]["authUrl"], mock_auth_url) self.assertEqual(response.data["data"]["state"], mock_state) mock_get_auth_url.assert_called_once_with(None) @patch("todo.services.google_oauth_service.GoogleOAuthService.get_authorization_url") - def test_get_with_redirect_url(self, mock_get_auth_url): + def test_get_returns_json_with_format_parameter(self, mock_get_auth_url): + """Test that format=json parameter returns JSON response""" + mock_auth_url = "https://accounts.google.com/o/oauth2/auth" + mock_state = "test_state" + mock_get_auth_url.return_value = (mock_auth_url, mock_state) + + response = self.client.get(f"{self.url}?format=json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["statusCode"], status.HTTP_200_OK) + self.assertEqual(response.data["message"], "Google OAuth URL generated successfully") + self.assertEqual(response.data["data"]["authUrl"], mock_auth_url) + self.assertEqual(response.data["data"]["state"], mock_state) + mock_get_auth_url.assert_called_once_with(None) + + @patch("todo.services.google_oauth_service.GoogleOAuthService.get_authorization_url") + def test_get_with_redirect_url_html_request(self, mock_get_auth_url): + """Test HTML request with redirect URL""" mock_auth_url = "https://accounts.google.com/o/oauth2/auth" mock_state = "test_state" mock_get_auth_url.return_value = (mock_auth_url, mock_state) @@ -58,6 +73,33 @@ def test_get_with_redirect_url(self, mock_get_auth_url): self.assertEqual(response.url, mock_auth_url) mock_get_auth_url.assert_called_once_with(redirect_url) + @patch("todo.services.google_oauth_service.GoogleOAuthService.get_authorization_url") + def test_get_with_redirect_url_json_request(self, mock_get_auth_url): + """Test JSON request with redirect URL""" + mock_auth_url = "https://accounts.google.com/o/oauth2/auth" + mock_state = "test_state" + mock_get_auth_url.return_value = (mock_auth_url, mock_state) + redirect_url = "http://localhost:3000/callback" + + response = self.client.get(f"{self.url}?redirectURL={redirect_url}", HTTP_ACCEPT="application/json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["data"]["authUrl"], mock_auth_url) + self.assertEqual(response.data["data"]["state"], mock_state) + mock_get_auth_url.assert_called_once_with(redirect_url) + + @patch("todo.services.google_oauth_service.GoogleOAuthService.get_authorization_url") + def test_stores_state_in_session(self, mock_get_auth_url): + """Test that state is stored in session for both request types""" + mock_auth_url = "https://accounts.google.com/o/oauth2/auth" + mock_state = "test_state" + mock_get_auth_url.return_value = (mock_auth_url, mock_state) + + response = self.client.get(self.url, HTTP_ACCEPT="application/json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.client.session.get("oauth_state"), mock_state) + class GoogleCallbackViewTests(APITestCase): def setUp(self): @@ -67,6 +109,8 @@ def setUp(self): self.factory = APIRequestFactory() self.view = GoogleCallbackView.as_view() + self.test_user_data = users_db_data[0] + def test_get_redirects_for_oauth_error(self): error = "access_denied" response = self.client.get(f"{self.url}?error={error}") @@ -80,67 +124,37 @@ def test_get_redirects_for_missing_code(self): self.assertEqual(response.status_code, status.HTTP_302_FOUND) self.assertIn("error=missing_code", response.url) - @patch("todo.services.google_oauth_service.GoogleOAuthService.handle_callback") - @patch("todo.services.user_service.UserService.create_or_update_user") - def test_get_redirects_for_valid_code_and_state(self, mock_create_user, mock_handle_callback): - mock_google_data = { - "id": "test_google_id", - "email": "test@example.com", - "name": "Test User", - } - user_id = str(ObjectId()) - mock_user = Mock() - mock_user.id = ObjectId(user_id) - mock_user.google_id = mock_google_data["id"] - mock_user.email_id = mock_google_data["email"] - mock_user.name = mock_google_data["name"] - type(mock_user).id = PropertyMock(return_value=ObjectId(user_id)) - - mock_handle_callback.return_value = mock_google_data - mock_create_user.return_value = mock_user - - session = self.client.session - session["oauth_state"] = "test_state" - session.save() - - response = self.client.get(f"{self.url}?code=test_code&state=test_state") + def test_get_redirects_for_missing_state(self): + response = self.client.get(f"{self.url}?code=test_code") self.assertEqual(response.status_code, status.HTTP_302_FOUND) - self.assertIn("success=true", response.url) - self.assertIn("ext-access", response.cookies) - self.assertIn("ext-refresh", response.cookies) - self.assertNotIn("oauth_state", self.client.session) - - def test_post_returns_error_for_missing_code(self): - response = self.client.post(self.url, {}) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data["message"], "No authorization code received from Google") + self.assertIn("error=missing_state", response.url) - def test_post_returns_error_for_invalid_state(self): + def test_get_redirects_for_invalid_state(self): session = self.client.session - session["oauth_state"] = "different_state" + session["oauth_state"] = "correct_state" session.save() - response = self.client.post(self.url, {"code": "test_code", "state": "invalid_state"}) + response = self.client.get(f"{self.url}?code=test_code&state=wrong_state") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data["message"], "Invalid state parameter") + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertIn("error=invalid_state", response.url) @patch("todo.services.google_oauth_service.GoogleOAuthService.handle_callback") @patch("todo.services.user_service.UserService.create_or_update_user") - def test_post_handles_callback_successfully(self, mock_create_user, mock_handle_callback): + def test_get_redirects_for_valid_code_and_state(self, mock_create_user, mock_handle_callback): mock_google_data = { - "id": "test_google_id", - "email": "test@example.com", - "name": "Test User", + "id": self.test_user_data["google_id"], + "email": self.test_user_data["email_id"], + "name": self.test_user_data["name"], } + user_id = str(ObjectId()) mock_user = Mock() mock_user.id = ObjectId(user_id) - mock_user.google_id = mock_google_data["id"] - mock_user.email_id = mock_google_data["email"] - mock_user.name = mock_google_data["name"] + mock_user.google_id = self.test_user_data["google_id"] + mock_user.email_id = self.test_user_data["email_id"] + mock_user.name = self.test_user_data["name"] type(mock_user).id = PropertyMock(return_value=ObjectId(user_id)) mock_handle_callback.return_value = mock_google_data @@ -150,50 +164,26 @@ def test_post_handles_callback_successfully(self, mock_create_user, mock_handle_ session["oauth_state"] = "test_state" session.save() - response = self.client.post(self.url, {"code": "test_code", "state": "test_state"}) + response = self.client.get(f"{self.url}?code=test_code&state=test_state") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["data"]["user"]["id"], user_id) - self.assertEqual(response.data["data"]["user"]["name"], mock_user.name) - self.assertEqual(response.data["data"]["user"]["email"], mock_user.email_id) - self.assertEqual(response.data["data"]["user"]["google_id"], mock_user.google_id) + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertIn("success=true", response.url) self.assertIn("ext-access", response.cookies) self.assertIn("ext-refresh", response.cookies) self.assertNotIn("oauth_state", self.client.session) -class GoogleRefreshViewTests(APITestCase): - def setUp(self): - super().setUp() - self.client = APIClient() - self.url = reverse("google_refresh") - - def test_get_returns_401_when_no_refresh_token(self): - response = self.client.get(self.url) - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertEqual(response.data["message"], AuthErrorMessages.NO_REFRESH_TOKEN) - self.assertEqual(response.data["authenticated"], False) - self.assertEqual(response.data["statusCode"], status.HTTP_401_UNAUTHORIZED) - - @patch("todo.utils.google_jwt_utils.validate_google_refresh_token") - def test_get_refreshes_token_successfully(self, mock_validate_token): - user_data = { - "user_id": str(ObjectId()), - "google_id": "test_google_id", - "email": "test@example.com", - "name": "Test User", - } - tokens = generate_google_token_pair(user_data) - mock_validate_token.return_value = user_data + @patch("todo.services.google_oauth_service.GoogleOAuthService.handle_callback") + def test_get_redirects_for_callback_exception(self, mock_handle_callback): + mock_handle_callback.side_effect = Exception("OAuth service error") - self.client.cookies["ext-refresh"] = tokens["refresh_token"] + session = self.client.session + session["oauth_state"] = "test_state" + session.save() - response = self.client.get(self.url, HTTP_ACCEPT="application/json") + response = self.client.get(f"{self.url}?code=test_code&state=test_state") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["data"]["success"], True) - self.assertEqual(response.data["message"], AppMessages.TOKEN_REFRESHED) - self.assertIn("ext-access", response.cookies) + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertIn("error=auth_failed", response.url) class GoogleLogoutViewTests(APITestCase): @@ -202,52 +192,57 @@ def setUp(self): self.client = APIClient() self.url = reverse("google_logout") - def test_get_returns_success_and_clears_cookies(self): - user_data = { - "user_id": str(ObjectId()), - "google_id": "test_google_id", - "email": "test@example.com", - "name": "Test User", - } - tokens = generate_google_token_pair(user_data) - self.client.cookies["ext-access"] = tokens["access_token"] - self.client.cookies["ext-refresh"] = tokens["refresh_token"] - - response = self.client.get(self.url, HTTP_ACCEPT="application/json") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["data"]["success"], True) - self.assertEqual(response.data["message"], AppMessages.GOOGLE_LOGOUT_SUCCESS) - self.assertEqual(response.cookies.get("ext-access").value, "") - self.assertEqual(response.cookies.get("ext-refresh").value, "") - - def test_get_redirects_when_not_json_request(self): + def test_get_returns_json_response(self): redirect_url = "http://localhost:3000" self.client.cookies["ext-access"] = "test_access_token" self.client.cookies["ext-refresh"] = "test_refresh_token" response = self.client.get(f"{self.url}?redirectURL={redirect_url}") - self.assertEqual(response.status_code, status.HTTP_302_FOUND) - self.assertEqual(response.url, redirect_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["data"]["success"], True) + self.assertEqual(response.data["message"], AppMessages.GOOGLE_LOGOUT_SUCCESS) self.assertEqual(response.cookies.get("ext-access").value, "") self.assertEqual(response.cookies.get("ext-refresh").value, "") def test_post_returns_success_and_clears_cookies(self): + """Test that POST requests return JSON""" user_data = { "user_id": str(ObjectId()), - "google_id": "test_google_id", - "email": "test@example.com", - "name": "Test User", + "google_id": google_auth_user_payload["google_id"], + "email": google_auth_user_payload["email"], + "name": google_auth_user_payload["name"], } tokens = generate_google_token_pair(user_data) self.client.cookies["ext-access"] = tokens["access_token"] self.client.cookies["ext-refresh"] = tokens["refresh_token"] - response = self.client.post(self.url, HTTP_ACCEPT="application/json") + response = self.client.post(self.url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["data"]["success"], True) self.assertEqual(response.data["message"], AppMessages.GOOGLE_LOGOUT_SUCCESS) self.assertEqual(response.cookies.get("ext-access").value, "") self.assertEqual(response.cookies.get("ext-refresh").value, "") + + def test_logout_clears_session(self): + """Test that logout clears session data""" + session = self.client.session + session["oauth_state"] = "test_state" + session["some_other_data"] = "test_data" + session.save() + + response = self.client.post(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertNotIn("oauth_state", self.client.session) + self.assertNotIn("some_other_data", self.client.session) + + def test_logout_clears_sessionid_cookie(self): + """Test that logout clears sessionid cookie""" + self.client.cookies["sessionid"] = "test_session_id" + + response = self.client.post(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.cookies.get("sessionid").value, "") diff --git a/todo/utils/google_jwt_utils.py b/todo/utils/google_jwt_utils.py index 08c54519..c4aa4375 100644 --- a/todo/utils/google_jwt_utils.py +++ b/todo/utils/google_jwt_utils.py @@ -28,7 +28,9 @@ def generate_google_access_token(user_data: dict) -> str: "token_type": "access", } - token = jwt.encode(payload=payload, key=settings.GOOGLE_JWT["PRIVATE_KEY"], algorithm=settings.GOOGLE_JWT["ALGORITHM"]) + token = jwt.encode( + payload=payload, key=settings.GOOGLE_JWT["PRIVATE_KEY"], algorithm=settings.GOOGLE_JWT["ALGORITHM"] + ) return token except Exception as e: @@ -50,7 +52,9 @@ def generate_google_refresh_token(user_data: dict) -> str: "email": user_data["email"], "token_type": "refresh", } - token = jwt.encode(payload=payload, key=settings.GOOGLE_JWT["PRIVATE_KEY"], algorithm=settings.GOOGLE_JWT["ALGORITHM"]) + token = jwt.encode( + payload=payload, key=settings.GOOGLE_JWT["PRIVATE_KEY"], algorithm=settings.GOOGLE_JWT["ALGORITHM"] + ) return token @@ -60,7 +64,9 @@ def generate_google_refresh_token(user_data: dict) -> str: def validate_google_access_token(token: str) -> dict: try: - payload = jwt.decode(jwt=token, key=settings.GOOGLE_JWT["PUBLIC_KEY"], algorithms=[settings.GOOGLE_JWT["ALGORITHM"]]) + payload = jwt.decode( + jwt=token, key=settings.GOOGLE_JWT["PUBLIC_KEY"], algorithms=[settings.GOOGLE_JWT["ALGORITHM"]] + ) if payload.get("token_type") != "access": raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) @@ -77,7 +83,9 @@ def validate_google_access_token(token: str) -> dict: def validate_google_refresh_token(token: str) -> dict: try: - payload = jwt.decode(jwt=token, key=settings.GOOGLE_JWT["PUBLIC_KEY"], algorithms=[settings.GOOGLE_JWT["ALGORITHM"]]) + payload = jwt.decode( + jwt=token, key=settings.GOOGLE_JWT["PUBLIC_KEY"], algorithms=[settings.GOOGLE_JWT["ALGORITHM"]] + ) if payload.get("token_type") != "refresh": raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) diff --git a/todo/views/auth.py b/todo/views/auth.py index adf81fa7..5995745e 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -11,6 +11,7 @@ from todo.utils.google_jwt_utils import generate_google_token_pair from todo.constants.messages import AppMessages + class GoogleLoginView(APIView): @extend_schema( operation_id="google_login", @@ -94,7 +95,7 @@ def get(self, request: Request): code = request.query_params.get("code") state = request.query_params.get("state") error = request.query_params.get("error") - + if error: frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" return HttpResponseRedirect(f"{frontend_callback}?error={error}") @@ -137,7 +138,6 @@ def get(self, request: Request): frontend_callback = f"{settings.FRONTEND_URL}/auth/callback" return HttpResponseRedirect(f"{frontend_callback}?error=auth_failed") - def _get_cookie_config(self): return { "path": "/", @@ -154,6 +154,7 @@ def _set_auth_cookies(self, response, tokens): "ext-refresh", tokens["refresh_token"], max_age=settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], **config ) + class GoogleLogoutView(APIView): @extend_schema( operation_id="google_logout", @@ -198,13 +199,15 @@ def post(self, request: Request): def _handle_logout(self, request: Request): request.session.flush() - - response = Response({ - "statusCode": status.HTTP_200_OK, - "message": AppMessages.GOOGLE_LOGOUT_SUCCESS, - "data": {"success": True}, - }) - + + response = Response( + { + "statusCode": status.HTTP_200_OK, + "message": AppMessages.GOOGLE_LOGOUT_SUCCESS, + "data": {"success": True}, + } + ) + self._clear_auth_cookies(response) return response @@ -226,7 +229,7 @@ def _clear_auth_cookies(self, response): response.delete_cookie("ext-refresh", **delete_config) session_delete_config = { - "path": getattr(settings, 'SESSION_COOKIE_PATH', '/'), - "domain": getattr(settings, 'SESSION_COOKIE_DOMAIN', None), + "path": getattr(settings, "SESSION_COOKIE_PATH", "/"), + "domain": getattr(settings, "SESSION_COOKIE_DOMAIN", None), } response.delete_cookie("sessionid", **session_delete_config) diff --git a/todo_project/settings/base.py b/todo_project/settings/base.py index 83877df3..af1be5c8 100644 --- a/todo_project/settings/base.py +++ b/todo_project/settings/base.py @@ -1,4 +1,5 @@ import os +import sys from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -119,13 +120,25 @@ "SCOPES": ["openid", "email", "profile"], } -GOOGLE_JWT = { - "ALGORITHM": "RS256", - "PRIVATE_KEY": os.getenv("GOOGLE_JWT_PRIVATE_KEY"), - "PUBLIC_KEY": os.getenv("GOOGLE_JWT_PUBLIC_KEY"), - "ACCESS_TOKEN_LIFETIME": int(os.getenv("GOOGLE_JWT_ACCESS_LIFETIME", "3600")), - "REFRESH_TOKEN_LIFETIME": int(os.getenv("GOOGLE_JWT_REFRESH_LIFETIME", "604800")), -} +TESTING = "test" in sys.argv or "pytest" in sys.modules or os.getenv("TESTING") == "True" + +if TESTING: + # Test JWT configuration (HS256 - simpler for tests) + GOOGLE_JWT = { + "ALGORITHM": "HS256", + "PRIVATE_KEY": "test-secret-key-for-jwt-signing-very-long-key-needed-for-security", + "PUBLIC_KEY": "test-secret-key-for-jwt-signing-very-long-key-needed-for-security", + "ACCESS_TOKEN_LIFETIME": int(os.getenv("GOOGLE_JWT_ACCESS_LIFETIME", "3600")), + "REFRESH_TOKEN_LIFETIME": int(os.getenv("GOOGLE_JWT_REFRESH_LIFETIME", "604800")), + } +else: + GOOGLE_JWT = { + "ALGORITHM": "RS256", + "PRIVATE_KEY": os.getenv("GOOGLE_JWT_PRIVATE_KEY"), + "PUBLIC_KEY": os.getenv("GOOGLE_JWT_PUBLIC_KEY"), + "ACCESS_TOKEN_LIFETIME": int(os.getenv("GOOGLE_JWT_ACCESS_LIFETIME", "3600")), + "REFRESH_TOKEN_LIFETIME": int(os.getenv("GOOGLE_JWT_REFRESH_LIFETIME", "604800")), + } GOOGLE_COOKIE_SETTINGS = { "ACCESS_COOKIE_NAME": os.getenv("GOOGLE_ACCESS_COOKIE_NAME", "ext-access"), diff --git a/todo_project/settings/development.py b/todo_project/settings/development.py index c4f6ac07..68632fbf 100644 --- a/todo_project/settings/development.py +++ b/todo_project/settings/development.py @@ -6,9 +6,9 @@ # Service ports configuration SERVICE_PORTS = { - "BACKEND": 3000, + "BACKEND": 8087, "AUTH": 8000, - "FRONTEND": 4000, + "FRONTEND": 3000, } # Base URL configuration