❌ PROBLEMA DETECTADO
Al hacer clic en "Login con Google" y luego en la ruta /api/auth/google/redirect/, Google retorna un error:
Error 400: redirect_uri_mismatch La URI de redireccionamiento en la solicitud: http://localhost:8000/api/auth/google/callback/ no coincide con una URI de redireccionamiento autorizada para el cliente de OAuth.
Causa raíz:
- Las URIs de redirección en Google Cloud Console NO coinciden con las del código
- Puede haber inconsistencias entre
localhosty127.0.0.1 - Las URIs autorizadas en Google deben ser EXACTAMENTE iguales
📋 SOLUCIÓN PASO A PASO
1 Verificar URLs en el código
Abre libros/oauth_views.py y verifica las URIs en AMBAS funciones:
Función google_oauth_callback (línea ~2804):
token_data = {
'code': code,
'client_id': google_config['client_id'],
'client_secret': google_config['secret'],
'redirect_uri': 'http://127.0.0.1:8000/api/auth/google/callback/', # ← VERIFICAR
'grant_type': 'authorization_code'
}
Función google_oauth_redirect (línea ~2918):
auth_url = (
'https://accounts.google.com/o/oauth2/v2/auth'
f'?client_id={google_config["client_id"]}'
f'&redirect_uri=http://127.0.0.1:8000/api/auth/google/callback/' # ← VERIFICAR
f'&scope={" ".join(scopes)}'
'&response_type=code'
'&access_type=offline'
'&prompt=consent'
)
⚠️ IMPORTANTE: Usa 127.0.0.1 en vez de localhost
Aunque ambos apuntan al mismo lugar, Google los trata como URIs diferentes.
Recomendación: Usa http://127.0.0.1:8000 consistentemente.
2 Configurar URIs en Google Cloud Console
- Ve a Google Cloud Console
- Navega a: APIs y servicios → Credenciales
- Click en tu ID de cliente de OAuth 2.0
- En la sección "URIs de redireccionamiento autorizados", agrega TODAS estas URIs:
http://127.0.0.1:8000/api/auth/google/callback/ http://localhost:8000/api/auth/google/callback/ http://127.0.0.1:8000/accounts/google/login/callback/ http://localhost:8000/accounts/google/login/callback/
💡 ¿Por qué agregar varias URIs?
- 127.0.0.1 y localhost: Para cubrir ambas variaciones
- /api/auth/google/callback/: Para tu endpoint personalizado
- /accounts/google/login/callback/: Para el flujo de django-allauth (por si necesitas usarlo)
5. Click en "GUARDAR"
6. Espera 5 minutos (Google tarda en propagar los cambios)
3 Actualizar código de oauth_views.py
Asegúrate que AMBAS funciones usen la misma URI. Abre libros/oauth_views.py y realiza los siguientes cambios:
✅ CAMBIO 1: Función google_oauth_callback - Soportar GET y obtener code dinámicamente
ANTES:
@api_view(['POST'])
@permission_classes([AllowAny])
def google_oauth_callback(request):
code = request.data.get('code')
DESPUÉS (SOLUCIÓN):
@api_view(['POST', 'GET']) # ← AGREGAR GET
@permission_classes([AllowAny])
def google_oauth_callback(request):
# Obtener code de POST o GET
code = request.data.get('code') or request.query_params.get('code') # ← CAMBIO AQUÍ
📝 ¿Por qué este cambio?
Google redirige con el código en la URL (método GET), no en el body (POST). Al agregar GET y buscar el code en query_params, recibimos correctamente el código de autorización.
✅ CAMBIO 2: Función google_oauth_redirect - Usar urlencode para construir URL
ANTES:
auth_url = (
'https://accounts.google.com/o/oauth2/v2/auth'
f'?client_id={google_config["client_id"]}'
f'&redirect_uri=http://127.0.0.1:8000/api/auth/google/callback/'
f'&scope={" ".join(scopes)}'
'&response_type=code'
'&access_type=offline'
'&prompt=consent'
)
DESPUÉS (SOLUCIÓN):
# Importar al inicio del archivo
from urllib.parse import urlencode
# En la función google_oauth_redirect:
params = {
'client_id': google_config["client_id"],
'redirect_uri': 'http://127.0.0.1:8000/api/auth/google/callback/',
'scope': " ".join(scopes),
'response_type': 'code',
'access_type': 'offline',
'prompt': 'consent',
}
auth_url = f'https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}'
📝 ¿Por qué este cambio?
La función urlencode() asegura que todos los parámetros estén correctamente codificados para URL, evitando errores por espacios o caracteres especiales en los scopes.
✅ CÓDIGO COMPLETO CORREGIDO:
from urllib.parse import urlencode # Agregar al inicio
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
@api_view(['POST', 'GET']) # ← SOPORTA GET
@permission_classes([AllowAny])
def google_oauth_callback(request):
"""Callback de Google OAuth - Recibe el código y genera JWT"""
# Obtener code de POST o GET
code = request.data.get('code') or request.query_params.get('code') # ← DINÁMICO
if not code:
return Response({
'error': 'Código de autorización no proporcionado'
}, status=400)
# Resto del código igual...
token_data = {
'code': code,
'client_id': google_config['client_id'],
'client_secret': google_config['secret'],
'redirect_uri': 'http://127.0.0.1:8000/api/auth/google/callback/',
'grant_type': 'authorization_code'
}
# ... continúa igual
@api_view(['GET'])
@permission_classes([AllowAny])
def google_oauth_redirect(request):
"""Redirige a Google para autenticación"""
scopes = [
'openid',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
# Construir URL con urlencode
params = {
'client_id': google_config["client_id"],
'redirect_uri': 'http://127.0.0.1:8000/api/auth/google/callback/',
'scope': " ".join(scopes),
'response_type': 'code',
'access_type': 'offline',
'prompt': 'consent',
}
auth_url = f'https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}' # ← URLENCODE
return Response({'auth_url': auth_url})
🔍 Verifica que sean IDÉNTICAS las URIs
Ambas funciones deben usar la misma redirect_uri exacta:
- ✅ Mismo protocolo (
http://) - ✅ Mismo host (
127.0.0.1olocalhost) - ✅ Mismo puerto (
:8000) - ✅ Mismo path (
/api/auth/google/callback/) - ✅ Con slash final (
/)
4 Verificar configuración en settings.py
Abre biblioteca_project/settings.py y verifica estas configuraciones:
ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # ← IMPORTANTE
# Configuración de Sites (debe estar)
SITE_ID = 1
# Configuración OAuth de Google
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': 'TU_CLIENT_ID_AQUI.apps.googleusercontent.com',
'secret': 'TU_SECRET_AQUI',
'key': ''
},
'SCOPE': [
'profile',
'email',
],
'AUTH_PARAMS': {
'access_type': 'online',
}
}
}
🔐 Verifica tus credenciales
Asegúrate que:
client_idtermina en.apps.googleusercontent.comsecretes un string alfanumérico largo- Ambos coinciden con los de Google Cloud Console
5 Agregar usuario de prueba en Google Console
- En Google Cloud Console, ve a: APIs y servicios → Pantalla de consentimiento de OAuth
- Scroll hasta "Usuarios de prueba"
- Click en "+ AGREGAR USUARIOS"
- Agrega tu email de Google (el que usarás para probar)
- Click en "GUARDAR"
⚠️ Solo usuarios de prueba pueden acceder
Mientras tu app esté en modo "Testing", solo los emails que agregues como "Test users" podrán hacer login.
6 Reiniciar servidor y probar
1. Detener el servidor si está corriendo:
2. Limpiar cache de Django (opcional pero recomendado):
3. Reiniciar servidor:
4. Probar flujo OAuth:
- Abre el navegador en modo incógnito (para limpiar cookies)
- Ve a:
http://127.0.0.1:8000/(NO uses localhost) - Click en "🔐 Login con Google"
- Te debe llevar a la página de Google
- Selecciona tu cuenta de Google (debe estar en test users)
- Acepta los permisos
- ✅ Debe redirigir exitosamente y mostrar tu token JWT
🔍 TROUBLESHOOTING ADICIONAL
Error: "Access blocked: This app's request is invalid"
Causa: Configuración incorrecta en Google Console
Solución:
- Verifica que OAuth consent screen esté completado
- Verifica que tengas al menos un Test user agregado
- Asegúrate que la app esté en modo "Testing" (no "In production")
Error: "redirect_uri_mismatch" sigue apareciendo
Solución:
- Verifica que hayas guardado los cambios en Google Console
- Espera 5-10 minutos (Google tarda en propagar cambios)
- Limpia cache del navegador o usa modo incógnito
- Verifica que estés usando 127.0.0.1 consistentemente (no localhost)
- Asegúrate que la URI termine con / (slash final)
Error: "The email is not in test users"
Solución:
- Ve a Google Console → OAuth consent screen → Test users
- Agrega el email exacto que estás usando para probar
- Guarda y espera unos minutos
✅ VERIFICACIÓN FINAL
🎯 Checklist de verificación
Antes de probar nuevamente, confirma que TODAS estas casillas estén marcadas:
- ☐ URIs en Google Console incluyen todas las variaciones (127.0.0.1, localhost, /api/auth/google/callback/)
- ☐
oauth_views.pyusahttp://127.0.0.1:8000/api/auth/google/callback/en ambas funciones - ☐
settings.pytieneALLOWED_HOSTS = ['localhost', '127.0.0.1'] - ☐ Client ID y Secret en
settings.pycoinciden con Google Console - ☐ Tu email está agregado como Test User en Google Console
- ☐ OAuth consent screen está completo (App name, Support email, Developer contact)
- ☐ Esperaste al menos 5 minutos después de cambios en Google Console
- ☐ Servidor Django está corriendo (
python manage.py runserver) - ☐ Estás accediendo vía
http://127.0.0.1:8000/(no localhost) - ☐ Navegador en modo incógnito (cookies limpias)
📊 FLUJO CORRECTO (para entenderlo)
🔄 Así debe funcionar el OAuth:
1. Usuario → Click "Login con Google" en http://127.0.0.1:8000/
↓
2. Frontend → GET /api/auth/google/redirect/
↓
3. Backend → Genera URL de Google y la devuelve
↓
4. Frontend → Redirige usuario a Google
↓
5. Google → Usuario autoriza permisos
↓
6. Google → Redirige a: http://127.0.0.1:8000/api/auth/google/callback/?code=XXX
↓
7. Backend → Recibe código, lo intercambia por access_token
↓
8. Backend → Usa access_token para obtener datos del usuario
↓
9. Backend → Crea/actualiza usuario en Django
↓
10. Backend → Genera JWT propio y lo devuelve
↓
11. Frontend → Guarda JWT en localStorage
↓
12. Usuario → Autenticado ✅
🎓 RESULTADO ESPERADO
✅ Si todo funciona correctamente, verás:
- Página de Google pidiendo autorización
- Seleccionas tu cuenta de Google
- Aceptas permisos
- Te redirige de vuelta a tu app (template oauth_login.html)
- Ves un mensaje: "Login exitoso con Google"
- Se muestra tu información:
- Nombre completo
- Foto de perfil
- Se genera un token JWT visible (completo)
- Botones "Ir a la API", "Volver al Inicio" y "Cerrar Sesión" funcionan
Token guardado en localStorage:
localStorage.access_token- Token de acceso JWTlocalStorage.refresh_token- Token de renovación
🔍 VERIFICAR TOKEN EN POSTMAN
Para verificar que el token JWT funciona correctamente:
1. Obtener el token de localStorage:
- Abre DevTools (F12) en el navegador
- Ve a la pestaña "Console"
- Ejecuta:
localStorage.getItem('access_token') - Copia el token que aparece
2. Probar en Postman:
a) Listar libros (GET):
GET http://127.0.0.1:8000/api/libros/ Headers: Authorization: Bearer TU_TOKEN_AQUI
b) Crear libro (POST):
POST http://127.0.0.1:8000/api/libros/
Headers:
Authorization: Bearer TU_TOKEN_AQUI
Content-Type: application/json
Body:
{
"titulo": "1984",
"isbn": "9780451524935",
"autor": 1,
"categoria": 1,
"stock": 5,
"precio": "350.00"
}
c) Actualizar libro (PATCH) - MÉTODO CORRECTO:
PATCH http://127.0.0.1:8000/api/libros/1/
Headers:
Authorization: Bearer TU_TOKEN_AQUI
Content-Type: application/json
Body:
{
"titulo": "1984 - Edición Especial",
"stock": 10
}
⚠️ IMPORTANTE: PATCH vs PUT
❌ Método PUT: Requiere TODOS los campos del modelo (reemplazo completo)
✅ Método PATCH: Permite actualizar solo campos específicos (actualización parcial)
Ejemplo:
- PUT: Debes enviar: titulo, isbn, autor, categoria, stock, precio, etc.
- PATCH: Solo envías los campos que quieres cambiar: {" titulo": "...", "stock": 10 }
Recomendación: Usa PATCH para actualizaciones parciales (más práctico)
d) Eliminar libro (DELETE):
DELETE http://127.0.0.1:8000/api/libros/5/ Headers: Authorization: Bearer TU_TOKEN_AQUI
🎓 MEJORA ADICIONAL: Template Completamente Funcional
🎉 ¡Éxito con OAuth + JWT!
Una vez que funcione, tendrás un sistema de autenticación moderno y profesional listo para producción.
📚 RECURSOS ADICIONALES
Si necesitas el template oauth_login.html completo y la función callback actualizada con redirect, consulta:
CORRECCION_OAUTH_TEMPLATE_COMPLETO.html
Incluye el código completo funcional con:
- ✅ Función google_oauth_callback() con redirect a template
- ✅ Template oauth_login.html completamente reescrito
- ✅ Manejo de múltiples casos (código, tokens, errores)
- ✅ Almacenamiento automático en localStorage
- ✅ Corrección PATCH vs PUT explicada