🔧 SOLUCIÓN ERROR OAUTH + JWT

Punto 3.8 - Error al redirigir desde Google OAuth

Problema: redirect_uri_mismatch

❌ 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 localhost y 127.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

  1. Ve a Google Cloud Console
  2. Navega a: APIs y servicios → Credenciales
  3. Click en tu ID de cliente de OAuth 2.0
  4. 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.1 o localhost)
  • ✅ 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_id termina en .apps.googleusercontent.com
  • secret es un string alfanumérico largo
  • Ambos coinciden con los de Google Cloud Console

5 Agregar usuario de prueba en Google Console

  1. En Google Cloud Console, ve a: APIs y servicios → Pantalla de consentimiento de OAuth
  2. Scroll hasta "Usuarios de prueba"
  3. Click en "+ AGREGAR USUARIOS"
  4. Agrega tu email de Google (el que usarás para probar)
  5. 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:

Ctrl + C

2. Limpiar cache de Django (opcional pero recomendado):

python manage.py migrate --run-syncdb

3. Reiniciar servidor:

python manage.py runserver

4. Probar flujo OAuth:

  1. Abre el navegador en modo incógnito (para limpiar cookies)
  2. Ve a: http://127.0.0.1:8000/ (NO uses localhost)
  3. Click en "🔐 Login con Google"
  4. Te debe llevar a la página de Google
  5. Selecciona tu cuenta de Google (debe estar en test users)
  6. Acepta los permisos
  7. ✅ 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:

  1. Verifica que hayas guardado los cambios en Google Console
  2. Espera 5-10 minutos (Google tarda en propagar cambios)
  3. Limpia cache del navegador o usa modo incógnito
  4. Verifica que estés usando 127.0.0.1 consistentemente (no localhost)
  5. 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.py usa http://127.0.0.1:8000/api/auth/google/callback/ en ambas funciones
  • settings.py tiene ALLOWED_HOSTS = ['localhost', '127.0.0.1']
  • ☐ Client ID y Secret en settings.py coinciden 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:

  1. Página de Google pidiendo autorización
  2. Seleccionas tu cuenta de Google
  3. Aceptas permisos
  4. Te redirige de vuelta a tu app (template oauth_login.html)
  5. Ves un mensaje: "Login exitoso con Google"
  6. Se muestra tu información:
    • Nombre completo
    • Email
    • Foto de perfil
  7. Se genera un token JWT visible (completo)
  8. Botones "Ir a la API", "Volver al Inicio" y "Cerrar Sesión" funcionan

Token guardado en localStorage:

  • localStorage.access_token - Token de acceso JWT
  • localStorage.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