Skip to content

Commit 3f88aea

Browse files
fix: adds RSA tokens and adjusted logic (#102)
* refactored google auth callback * fix: failing tests * fix: login issue * removed token lifetime * added rsa token and adjusted logic accordingly * 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
1 parent e0d50b8 commit 3f88aea

File tree

11 files changed

+268
-275
lines changed

11 files changed

+268
-275
lines changed

.env.example

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,10 @@ RDS_BACKEND_BASE_URL='http://localhost:3000'
77
RDS_PUBLIC_KEY="public-key-here"
88
GOOGLE_OAUTH_CLIENT_ID="google-client-id"
99
GOOGLE_OAUTH_CLIENT_SECRET="client-secret"
10-
GOOGLE_OAUTH_REDIRECT_URI="environment-url/auth/google/callback"
11-
GOOGLE_JWT_SECRET_KEY=generate-secret-key
10+
# Google JWT RSA Keys
11+
GOOGLE_JWT_PRIVATE_KEY="generate keys and paste here"
12+
GOOGLE_JWT_PUBLIC_KEY="generate keys and paste here"
13+
14+
# use if required
15+
# GOOGLE_JWT_ACCESS_LIFETIME="20"
16+
# GOOGLE_JWT_REFRESH_LIFETIME="30"

.github/workflows/test.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ jobs:
1212
env:
1313
MONGODB_URI: mongodb://db:27017
1414
DB_NAME: todo-app
15-
GOOGLE_JWT_SECRET_KEY: "test-secret-key-for-jwt"
1615
GOOGLE_JWT_ACCESS_LIFETIME: "3600"
1716
GOOGLE_JWT_REFRESH_LIFETIME: "604800"
1817
GOOGLE_OAUTH_CLIENT_ID: "test-client-id"
1918
GOOGLE_OAUTH_CLIENT_SECRET: "test-client-secret"
20-
GOOGLE_OAUTH_REDIRECT_URI: "http://localhost:3000/auth/callback"
2119
COOKIE_SECURE: "False"
2220
COOKIE_SAMESITE: "Lax"
2321

todo/middlewares/jwt_auth.py

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33
from django.http import JsonResponse
44

55
from todo.utils.jwt_utils import verify_jwt_token
6-
from todo.utils.google_jwt_utils import validate_google_access_token
6+
from todo.utils.google_jwt_utils import (
7+
validate_google_access_token,
8+
validate_google_refresh_token,
9+
generate_google_access_token,
10+
)
711
from todo.exceptions.auth_exceptions import TokenMissingError, TokenExpiredError, TokenInvalidError
8-
from todo.exceptions.google_auth_exceptions import GoogleTokenExpiredError, GoogleTokenInvalidError
12+
from todo.exceptions.google_auth_exceptions import (
13+
GoogleTokenExpiredError,
14+
GoogleTokenInvalidError,
15+
GoogleRefreshTokenExpiredError,
16+
)
917
from todo.constants.messages import AuthErrorMessages, ApiErrors
1018
from todo.dto.responses.error_response import ApiErrorResponse, ApiErrorDetail
1119

@@ -25,7 +33,8 @@ def __call__(self, request):
2533
auth_success = self._try_authentication(request)
2634

2735
if auth_success:
28-
return self.get_response(request)
36+
response = self.get_response(request)
37+
return self._process_response(request, response)
2938
else:
3039
error_response = ApiErrorResponse(
3140
statusCode=status.HTTP_401_UNAUTHORIZED,
@@ -73,25 +82,61 @@ def _try_google_auth(self, request) -> bool:
7382
try:
7483
google_token = request.COOKIES.get("ext-access")
7584

76-
if not google_token:
85+
if google_token:
86+
try:
87+
payload = validate_google_access_token(google_token)
88+
self._set_google_user_data(request, payload)
89+
return True
90+
except (GoogleTokenExpiredError, GoogleTokenInvalidError):
91+
pass
92+
93+
return self._try_google_refresh(request)
94+
95+
except (GoogleTokenExpiredError, GoogleTokenInvalidError) as e:
96+
raise e
97+
except Exception:
98+
return False
99+
100+
def _try_google_refresh(self, request) -> bool:
101+
"""Try to refresh Google access token"""
102+
try:
103+
refresh_token = request.COOKIES.get("ext-refresh")
104+
105+
if not refresh_token:
77106
return False
78107

79-
payload = validate_google_access_token(google_token)
108+
payload = validate_google_refresh_token(refresh_token)
80109

81-
request.auth_type = "google"
82-
request.user_id = payload["user_id"]
83-
request.google_id = payload["google_id"]
84-
request.user_email = payload["email"]
85-
request.user_name = payload["name"]
86-
request.user_role = "external_user"
110+
user_data = {
111+
"user_id": payload["user_id"],
112+
"google_id": payload["google_id"],
113+
"email": payload["email"],
114+
"name": payload.get("name", ""),
115+
}
116+
117+
new_access_token = generate_google_access_token(user_data)
118+
119+
self._set_google_user_data(request, payload)
120+
121+
request._new_access_token = new_access_token
122+
request._access_token_expires = settings.GOOGLE_JWT["ACCESS_TOKEN_LIFETIME"]
87123

88124
return True
89125

90-
except (GoogleTokenExpiredError, GoogleTokenInvalidError) as e:
91-
raise e
126+
except (GoogleRefreshTokenExpiredError, GoogleTokenInvalidError):
127+
return False
92128
except Exception:
93129
return False
94130

131+
def _set_google_user_data(self, request, payload):
132+
"""Set Google user data on request"""
133+
request.auth_type = "google"
134+
request.user_id = payload["user_id"]
135+
request.google_id = payload["google_id"]
136+
request.user_email = payload["email"]
137+
request.user_name = payload.get("name", "")
138+
request.user_role = "external_user"
139+
95140
def _try_rds_auth(self, request) -> bool:
96141
try:
97142
rds_token = request.COOKIES.get(self.rds_cookie_name)
@@ -112,6 +157,25 @@ def _try_rds_auth(self, request) -> bool:
112157
except Exception:
113158
return False
114159

160+
def _process_response(self, request, response):
161+
"""Process response and set new cookies if Google token was refreshed"""
162+
if hasattr(request, "_new_access_token"):
163+
config = self._get_cookie_config()
164+
response.set_cookie(
165+
"ext-access", request._new_access_token, max_age=request._access_token_expires, **config
166+
)
167+
return response
168+
169+
def _get_cookie_config(self):
170+
"""Get Google cookie configuration"""
171+
return {
172+
"path": "/",
173+
"domain": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_DOMAIN"),
174+
"secure": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SECURE", False),
175+
"httponly": True,
176+
"samesite": settings.GOOGLE_COOKIE_SETTINGS.get("COOKIE_SAMESITE", "Lax"),
177+
}
178+
115179
def _is_public_path(self, path: str) -> bool:
116180
return any(path.startswith(public_path) for public_path in settings.PUBLIC_PATHS)
117181

todo/tests/integration/base_mongo_test.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ def setUpClass(cls):
2020
cls.db = cls.mongo_client.get_database("testdb")
2121

2222
cls.override = override_settings(
23-
MONGODB_URI=cls.mongo_url,
24-
DB_NAME="testdb",
23+
MONGODB_URI=cls.mongo_url, DB_NAME="testdb", FRONTEND_URL="http://localhost:4000"
2524
)
2625
cls.override.enable()
2726
DatabaseManager.reset()

todo/tests/unit/middlewares/test_jwt_auth.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from unittest import TestCase
22
from unittest.mock import Mock, patch
33
from django.http import HttpRequest, JsonResponse
4-
from django.conf import settings
54
from rest_framework import status
65
import json
76

@@ -17,9 +16,6 @@ def setUp(self):
1716
self.request.path = "/v1/tasks"
1817
self.request.headers = {}
1918
self.request.COOKIES = {}
20-
self._original_public_paths = settings.PUBLIC_PATHS
21-
settings.PUBLIC_PATHS = ["/v1/auth/google/login"]
22-
self.addCleanup(setattr, settings, "PUBLIC_PATHS", self._original_public_paths)
2319

2420
def test_public_path_authentication_bypass(self):
2521
"""Test that requests to public paths bypass authentication"""

0 commit comments

Comments
 (0)