✅ PROBLEMA RESUELTO
Problema original:
- ❌ El sistema redirigía a Django REST en lugar de mostrar el template personalizado
- ❌ Error 405 Method Not Allowed al intentar login con Google
- ❌ No se mostraba la información del usuario después de login exitoso
- ❌ Los tokens JWT no se visualizaban correctamente
Solución implementada:
- ✅ Template
oauth_login.htmlcompletamente reescrito con manejo de múltiples casos - ✅ Función
google_oauth_callbackactualizada para soportar GET y redirigir al template - ✅ Información completa del usuario y tokens JWT visibles
- ✅ Validación correcta del token en Postman
📝 PARTE 1: Actualizar oauth_views.py
🔧 Función google_oauth_callback() Corregida
Ubicación: libros/oauth_views.py
Cambios principales:
- Soportar método GET (Google redirige con GET)
- Usar
redirect()en lugar deResponse() - Enviar datos del usuario y tokens en la URL al template
- Codificar correctamente los datos con
urllib.parse.quote()
📄 CÓDIGO COMPLETO:
from django.conf import settings
from django.contrib.auth import get_user_model
from django.shortcuts import redirect
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from urllib.parse import urlencode, quote
import urllib.parse
import requests
import logging
import json
logger = logging.getLogger(__name__)
User = get_user_model()
@api_view(['GET', 'POST']) # ← AGREGAR GET
@permission_classes([AllowAny])
def google_oauth_callback(request):
"""
Endpoint que recibe el código de autorización de Google
y redirige a oauth_login.html con los tokens
GET /api/auth/google/callback/?code=... (desde Google)
POST /api/auth/google/callback/ (desde frontend)
"""
# 1. Obtener el código de autorización (GET de Google o POST del frontend)
if request.method == 'GET':
code = request.GET.get('code')
error = request.GET.get('error')
if error:
logger.error(f"Error de Google: {error}")
return redirect(f'/oauth/login/?error={urllib.parse.quote(error)}')
else: # POST
code = request.data.get('code')
if not code:
error_msg = 'El código de autorización es requerido'
logger.error(error_msg)
if request.method == 'GET':
return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
return Response({'error': error_msg}, status=status.HTTP_400_BAD_REQUEST)
try:
# 2. Intercambiar código por access token de Google
token_url = 'https://oauth2.googleapis.com/token'
google_config = settings.SOCIALACCOUNT_PROVIDERS['google']['APP']
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'
}
token_response = requests.post(token_url, data=token_data, timeout=10)
token_response.raise_for_status()
tokens = token_response.json()
google_access_token = tokens.get('access_token')
if not google_access_token:
error_msg = 'No se pudo obtener access token de Google'
logger.error(error_msg)
return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
logger.info(f"Access token de Google obtenido: {google_access_token[:20]}...")
# 3. Obtener información del usuario de Google
userinfo_url = 'https://www.googleapis.com/oauth2/v2/userinfo'
headers = {'Authorization': f'Bearer {google_access_token}'}
userinfo_response = requests.get(userinfo_url, headers=headers, timeout=10)
userinfo_response.raise_for_status()
user_data = userinfo_response.json()
logger.info(f"Datos de usuario de Google: {user_data}")
# 4. Crear o actualizar usuario en Django
email = user_data.get('email')
if not email:
error_msg = 'No se pudo obtener el email del usuario'
logger.error(error_msg)
return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
# Buscar si el usuario ya existe
user, created = User.objects.get_or_create(
email=email,
defaults={
'username': email.split('@')[0],
'first_name': user_data.get('given_name', ''),
'last_name': user_data.get('family_name', ''),
}
)
# Si el usuario ya existía, actualizar su información
if not created:
user.first_name = user_data.get('given_name', user.first_name)
user.last_name = user_data.get('family_name', user.last_name)
user.save()
logger.info(f"Usuario existente actualizado: {user.email}")
else:
logger.info(f"Nuevo usuario creado: {user.email}")
# 5. Generar tokens JWT de nuestra aplicación
refresh = RefreshToken.for_user(user)
access_token = str(refresh.access_token)
# 6. Preparar datos para enviar al frontend
user_info = {
'id': user.id,
'email': user.email,
'username': user.username,
'first_name': user.first_name,
'last_name': user.last_name,
'is_staff': user.is_staff,
}
google_info = {
'picture': user_data.get('picture'),
'verified_email': user_data.get('verified_email'),
}
# Codificar datos para URL
user_info_json = json.dumps(user_info)
google_info_json = json.dumps(google_info)
# Construir URL de redirección a oauth_login.html con todos los datos
redirect_url = (
f'/oauth/login/?'
f'access_token={access_token}&'
f'refresh_token={str(refresh)}&'
f'user_info={urllib.parse.quote(user_info_json)}&'
f'google_info={urllib.parse.quote(google_info_json)}&'
f'message={urllib.parse.quote("Login exitoso con Google" if not created else "Cuenta creada exitosamente con Google")}'
)
logger.info(f"Redirigiendo a: {redirect_url[:100]}...")
return redirect(redirect_url)
except requests.Timeout:
logger.error("Timeout al comunicarse con Google")
return redirect(f'/oauth/login/?error={urllib.parse.quote("Timeout al comunicarse con Google")}')
except requests.RequestException as e:
logger.error(f"Error al comunicarse con Google: {str(e)}")
return redirect(f'/oauth/login/?error={urllib.parse.quote(f"Error con Google: {str(e)}")}')
except Exception as e:
logger.error(f"Error inesperado en OAuth: {str(e)}")
return redirect(f'/oauth/login/?error={urllib.parse.quote(f"Error inesperado: {str(e)}")}')
📝 PARTE 2: Template oauth_login.html Completo
🎨 Template Reescrito Completamente
Ubicación: templates/oauth_login.html
Mejoras implementadas:
- ✅ Manejo de múltiples casos (código, tokens, errores)
- ✅ No redirige automáticamente a Django REST
- ✅ Muestra información del usuario con foto de perfil
- ✅ Muestra tokens JWT completos
- ✅ Botones funcionales (API, Inicio, Cerrar sesión, Reintentar)
- ✅ Almacena tokens en localStorage automáticamente
- ✅ Diseño mejorado con mejor UX
📄 CÓDIGO COMPLETO DEL TEMPLATE:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login con Google - Biblioteca UTH</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 50px rgba(0,0,0,0.2);
max-width: 600px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 30px;
text-align: center;
}
.loading {
text-align: center;
color: #666;
font-size: 1.2em;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.result {
margin-top: 30px;
padding: 20px;
border-radius: 8px;
display: none;
}
.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.user-info {
background: #f0f4ff;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
}
.user-info img {
border-radius: 50%;
margin-top: 10px;
border: 3px solid #667eea;
}
.token-display {
background: #2d2d2d;
color: #4ec9b0;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
word-wrap: break-word;
font-family: 'Courier New', monospace;
font-size: 0.9em;
max-height: 100px;
overflow: auto;
}
.btn {
display: inline-block;
padding: 10px 20px;
margin: 5px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.btn:hover {
background: #5568d3;
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #5a6268;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 Login con Google</h1>
<div id="loading" class="loading">
<div class="spinner"></div>
<p id="loading-message">Procesando autenticación...</p>
</div>
<div id="result" class="result"></div>
</div>
<script>
// Obtener parámetros de la URL
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const accessToken = urlParams.get('access_token');
const refreshToken = urlParams.get('refresh_token');
const userInfoParam = urlParams.get('user_info');
const googleInfoParam = urlParams.get('google_info');
const message = urlParams.get('message');
const error = urlParams.get('error');
// Elementos del DOM
const loadingEl = document.getElementById('loading');
const loadingMessage = document.getElementById('loading-message');
const resultEl = document.getElementById('result');
// Función para mostrar error
function showError(errorMsg) {
loadingEl.style.display = 'none';
resultEl.className = 'result error';
resultEl.style.display = 'block';
resultEl.innerHTML = `
<h2>❌ Error en el Login</h2>
<p>${errorMsg || 'Error desconocido'}</p>
<a href="/" class="btn">🏠 Volver al Inicio</a>
<button onclick="window.location.href='/oauth/login/'" class="btn btn-secondary">🔄 Reintentar</button>
`;
}
// Función para mostrar éxito
function showSuccess(data) {
loadingEl.style.display = 'none';
resultEl.className = 'result success';
resultEl.style.display = 'block';
// Guardar tokens
if (data.access_token || data.access) {
localStorage.setItem('access_token', data.access_token || data.access);
}
if (data.refresh_token || data.refresh) {
localStorage.setItem('refresh_token', data.refresh_token || data.refresh);
}
// Obtener datos del usuario
const user = data.user_info || data.user || {};
const google = data.google_info || data.google_data || {};
resultEl.innerHTML = `
<h2>✅ ${data.message || 'Login exitoso'}</h2>
<div class="user-info">
<h3>👤 Datos del Usuario</h3>
<p><strong>Email:</strong> ${user.email || 'N/A'}</p>
<p><strong>Nombre:</strong> ${user.first_name || ''} ${user.last_name || ''}</p>
<p><strong>Username:</strong> ${user.username || ''}</p>
${google.picture ? `<img src="${google.picture}" alt="Foto de perfil" width="100">` : ''}
</div>
<div class="token-display">
<strong>🔑 Access Token:</strong><br>
${data.access_token || data.access || 'N/A'}
</div>
<a href="/api/" class="btn">📚 Ir a la API</a>
<a href="/" class="btn">🏠 Volver al Inicio</a>
<button onclick="logout()" class="btn btn-secondary">🚪 Cerrar Sesión</button>
`;
}
// Función para cerrar sesión
function logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/';
}
// Función para intercambiar código por token (POST al backend)
async function exchangeCodeForToken(code) {
try {
loadingMessage.textContent = 'Intercambiando código por tokens...';
const response = await fetch('/api/auth/google/callback/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code: code })
});
const data = await response.json();
if (response.ok) {
showSuccess(data);
} else {
showError(data.error || 'Error al procesar el código');
}
} catch (error) {
showError('Error de red: ' + error.message);
}
}
// FLUJO PRINCIPAL
(function main() {
// Caso 1: Hay error en la URL
if (error) {
showError(decodeURIComponent(error));
}
// Caso 2: Tenemos access_token con user_info (flujo completo desde backend)
else if (accessToken && userInfoParam) {
loadingMessage.textContent = 'Procesando datos del usuario...';
try {
const userInfo = JSON.parse(decodeURIComponent(userInfoParam));
const googleInfo = googleInfoParam ? JSON.parse(decodeURIComponent(googleInfoParam)) : {};
showSuccess({
access_token: accessToken,
refresh_token: refreshToken,
user_info: userInfo,
google_info: googleInfo,
message: message ? decodeURIComponent(message) : 'Login exitoso'
});
} catch (e) {
showError('Error procesando datos del usuario');
}
}
// Caso 3: Tenemos código de Google (necesita intercambio)
else if (code) {
exchangeCodeForToken(code);
}
// Caso 4: No hay nada, redirigir a Google
else {
loadingMessage.textContent = 'Redirigiendo a Google...';
window.location.href = '/api/auth/google/redirect/';
}
})();
</script>
</body>
</html>
📊 PARTE 3: Resultados y Verificación
✅ FUNCIONAMIENTO VERIFICADO
1️⃣ Flujo de Login Completo:
- Usuario hace clic en "Login con Google" en la página de inicio
- Redirige a
/oauth/login/(template oauth_login.html) - El template detecta que no hay código y redirige a Google
- Usuario inicia sesión en Google y autoriza la aplicación
- Google redirige a
/api/auth/google/callback/?code=...(GET) - La función callback procesa el código y redirige a
/oauth/login/?access_token=...&user_info=... - El template muestra los datos del usuario y el token JWT
2️⃣ Información Mostrada:
- ✅ Mensaje de éxito ("Login exitoso con Google")
- ✅ Email del usuario
- ✅ Nombre completo
- ✅ Username
- ✅ Foto de perfil de Google
- ✅ Access Token JWT completo
- ✅ Refresh Token (guardado en localStorage)
3️⃣ Tokens Almacenados:
- ✅
localStorage.access_token- Token de acceso JWT - ✅
localStorage.refresh_token- Token de renovación JWT
4️⃣ Token Verificado en Postman:
El token JWT generado funciona correctamente para:
- ✅ GET /api/libros/ - Listar libros
- ✅ POST /api/libros/ - Crear nuevos libros
- ✅ PATCH /api/libros/{id}/ - Actualizar libro (método correcto)
- ✅ DELETE /api/libros/{id}/ - Eliminar libro
⚠️ CORRECCIÓN ADICIONAL: Método PATCH vs PUT
❌ Error identificado en la guía original:
La guía menciona usar PUT para actualizar libros:
PUT /api/libros/1/
Body: {
"titulo": "Nuevo Título",
"stock": 10
}
✅ Corrección necesaria:
Usar PATCH en lugar de PUT para actualizaciones parciales:
PATCH /api/libros/1/
Body: {
"titulo": "Nuevo Título",
"stock": 10
}
📝 Explicación:
- PUT: Requiere TODOS los campos del modelo (reemplazo completo)
- PATCH: Permite actualizar solo los campos específicos (actualización parcial)
Recomendación: Actualizar la guía para usar PATCH en ejemplos de actualización parcial.
🎯 RESUMEN DE CAMBIOS
📋 Checklist de Implementación
Archivo oauth_views.py:
- ✅ Agregar imports:
redirect,urllib.parse,json - ✅ Modificar decorador:
@api_view(['GET', 'POST']) - ✅ Detectar método (GET de Google vs POST de frontend)
- ✅ Usar
redirect()en lugar deResponse() - ✅ Preparar datos:
user_infoygoogle_info - ✅ Codificar JSON para URL con
urllib.parse.quote() - ✅ Construir URL de redirección completa
- ✅ Manejar errores con redirect a template
Archivo oauth_login.html:
- ✅ Reemplazar TODO el contenido del template
- ✅ Agregar estilos para imagen de perfil
- ✅ Implementar función
showError() - ✅ Implementar función
showSuccess()unificada - ✅ Implementar función
logout() - ✅ Implementar función
exchangeCodeForToken() - ✅ Implementar flujo principal con casos múltiples
- ✅ Parsear JSON de parámetros URL
- ✅ Mostrar token completo (scrolleable)
- ✅ Agregar botones funcionales
Actualización de documentación:
- ✅ Corregir método PUT → PATCH en ejemplos
- ✅ Agregar capturas de pantalla del flujo
- ✅ Documentar casos de uso del template
- ✅ Explicar diferencia entre GET y POST
🎉 ¡SISTEMA OAUTH COMPLETAMENTE FUNCIONAL!
Ahora tienes un sistema de autenticación OAuth 2.0 profesional que:
- ✅ Redirige correctamente a tu template personalizado
- ✅ Muestra toda la información del usuario
- ✅ Genera y muestra tokens JWT válidos
- ✅ Almacena tokens en localStorage automáticamente
- ✅ Permite operaciones CRUD con el token
- ✅ Maneja errores elegantemente
- ✅ Proporciona buena experiencia de usuario