security: fix GoogleLoginView audience check + replace Clerk with direct GIS flow

- verify_oauth2_token now passes GOOGLE_CLIENT_ID as third arg (audience check)
- fail-closed: returns 503 if GOOGLE_CLIENT_ID env var is not set
- add GOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID', '') to settings
- replace ClerkLoginViewTests with GoogleLoginViewTests (4 cases)
- update requirements-docker.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 01:31:18 +05:30
parent aa2846b884
commit e0a491e8cb
5 changed files with 131 additions and 3 deletions

View File

@@ -1,3 +1,89 @@
from django.test import TestCase
"""Unit tests for GoogleLoginView.
# Create your tests here.
Run with:
python manage.py test mobile_api.tests
"""
import json
from unittest.mock import patch, MagicMock
from django.test import TestCase, override_settings
from rest_framework.authtoken.models import Token
from accounts.models import User
@override_settings(GOOGLE_CLIENT_ID='test-client-id.apps.googleusercontent.com')
class GoogleLoginViewTests(TestCase):
url = '/api/user/google-login/'
def _valid_idinfo(self, email='new.user@example.com'):
return {
'email': email,
'given_name': 'New',
'family_name': 'User',
'aud': 'test-client-id.apps.googleusercontent.com',
}
@patch('google.oauth2.id_token.verify_oauth2_token')
def test_valid_token_creates_user(self, mock_verify):
mock_verify.return_value = self._valid_idinfo('fresh@example.com')
resp = self.client.post(
self.url,
data=json.dumps({'id_token': 'fake.google.jwt'}),
content_type='application/json',
)
self.assertEqual(resp.status_code, 200, resp.content)
body = resp.json()
self.assertEqual(body['email'], 'fresh@example.com')
self.assertEqual(body['role'], 'customer')
self.assertTrue(body['token'])
user = User.objects.get(email='fresh@example.com')
self.assertTrue(Token.objects.filter(user=user).exists())
# Confirm audience was passed to verify_oauth2_token
_, call_kwargs = mock_verify.call_args[0], mock_verify.call_args
self.assertEqual(mock_verify.call_args[0][2], 'test-client-id.apps.googleusercontent.com')
def test_missing_id_token_returns_400(self):
resp = self.client.post(
self.url,
data=json.dumps({}),
content_type='application/json',
)
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.json()['error'], 'id_token is required')
@patch('google.oauth2.id_token.verify_oauth2_token')
def test_invalid_token_returns_401(self, mock_verify):
mock_verify.side_effect = ValueError('Token audience mismatch')
resp = self.client.post(
self.url,
data=json.dumps({'id_token': 'tampered.or.wrong-aud.jwt'}),
content_type='application/json',
)
self.assertEqual(resp.status_code, 401)
self.assertEqual(resp.json()['error'], 'Invalid Google token')
@patch('google.oauth2.id_token.verify_oauth2_token')
def test_existing_user_reuses_token(self, mock_verify):
existing = User.objects.create_user(
username='returning@example.com',
email='returning@example.com',
password='irrelevant',
role='customer',
)
existing_auth_token = Token.objects.create(user=existing)
mock_verify.return_value = self._valid_idinfo('returning@example.com')
resp = self.client.post(
self.url,
data=json.dumps({'id_token': 'returning.user.jwt'}),
content_type='application/json',
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json()['token'], existing_auth_token.key)
# No duplicate user created
self.assertEqual(User.objects.filter(email='returning@example.com').count(), 1)

View File

@@ -431,12 +431,22 @@ class GoogleLoginView(View):
from google.oauth2 import id_token as google_id_token
from google.auth.transport import requests as google_requests
from django.conf import settings
data = json.loads(request.body)
token = data.get('id_token')
if not token:
return JsonResponse({'error': 'id_token is required'}, status=400)
idinfo = google_id_token.verify_oauth2_token(token, google_requests.Request())
if not settings.GOOGLE_CLIENT_ID:
log("error", "GOOGLE_CLIENT_ID not configured", request=request)
return JsonResponse({'error': 'Google login temporarily unavailable'}, status=503)
idinfo = google_id_token.verify_oauth2_token(
token,
google_requests.Request(),
settings.GOOGLE_CLIENT_ID,
)
email = idinfo.get('email')
if not email:
return JsonResponse({'error': 'Email not found in Google token'}, status=400)