feat: enhance authentication system with token-based login and session management
- Updated the backend to support token-based authentication, replacing basic auth. - Added session management functions for creating, invalidating, and refreshing sessions. - Refactored user routes to include login and logout endpoints. - Modified frontend to handle token storage and session validation. - Improved user experience by ensuring sessions are invalidated upon password changes.
This commit is contained in:
@@ -13,6 +13,11 @@ RUN npm ci --only=production
|
||||
|
||||
# Copiar código de la aplicación
|
||||
COPY server.js .
|
||||
COPY config/ ./config/
|
||||
COPY middlewares/ ./middlewares/
|
||||
COPY routes/ ./routes/
|
||||
COPY services/ ./services/
|
||||
COPY utils/ ./utils/
|
||||
|
||||
# Exponer puerto
|
||||
EXPOSE 3001
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
import { getRedisClient } from '../services/redis.js';
|
||||
|
||||
// Autenticación básica Middleware
|
||||
export async function basicAuthMiddleware(req, res, next) {
|
||||
// Duración de la sesión en segundos (24 horas)
|
||||
const SESSION_DURATION = 24 * 60 * 60;
|
||||
|
||||
// Generar token seguro
|
||||
function generateToken() {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
// Autenticación por token Middleware
|
||||
export async function authMiddleware(req, res, next) {
|
||||
const redisClient = getRedisClient();
|
||||
|
||||
if (!redisClient) {
|
||||
@@ -12,50 +20,43 @@ export async function basicAuthMiddleware(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
// NO enviar WWW-Authenticate para evitar el diálogo nativo del navegador
|
||||
// En su lugar, devolveremos un 401 y el frontend manejará el modal personalizado
|
||||
return res.status(401).json({ error: 'Authentication required', message: 'Se requiere autenticación para esta operación' });
|
||||
}
|
||||
|
||||
const [scheme, encoded] = authHeader.split(' ');
|
||||
const [scheme, token] = authHeader.split(' ');
|
||||
|
||||
if (scheme !== 'Basic') {
|
||||
return res.status(400).json({ error: 'Bad request', message: 'Solo se admite autenticación Basic' });
|
||||
if (scheme !== 'Bearer' || !token) {
|
||||
return res.status(400).json({ error: 'Bad request', message: 'Se requiere un token Bearer' });
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = Buffer.from(encoded, 'base64');
|
||||
const [username, password] = buffer.toString().split(':');
|
||||
// Verificar token en Redis
|
||||
const sessionKey = `session:${token}`;
|
||||
const sessionData = await redisClient.get(sessionKey);
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña no proporcionados' });
|
||||
if (!sessionData) {
|
||||
return res.status(401).json({ error: 'Invalid token', message: 'Token inválido o sesión expirada' });
|
||||
}
|
||||
|
||||
// Buscar usuario en Redis
|
||||
const userKey = `user:${username}`;
|
||||
// Parsear datos de sesión
|
||||
const session = JSON.parse(sessionData);
|
||||
|
||||
// Verificar que el usuario aún existe
|
||||
const userKey = `user:${session.username}`;
|
||||
const userExists = await redisClient.exists(userKey);
|
||||
|
||||
if (!userExists) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||
// Eliminar sesión si el usuario ya no existe
|
||||
await redisClient.del(sessionKey);
|
||||
return res.status(401).json({ error: 'Invalid token', message: 'Usuario no encontrado' });
|
||||
}
|
||||
|
||||
// Obtener hash de la contraseña
|
||||
const userData = await redisClient.hGetAll(userKey);
|
||||
const passwordHash = userData.passwordHash;
|
||||
|
||||
if (!passwordHash) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||
}
|
||||
|
||||
// Verificar contraseña
|
||||
const match = await bcrypt.compare(password, passwordHash);
|
||||
|
||||
if (!match) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||
}
|
||||
// Actualizar TTL de la sesión (refresh)
|
||||
await redisClient.expire(sessionKey, SESSION_DURATION);
|
||||
|
||||
// Autenticación exitosa
|
||||
req.user = { username };
|
||||
req.user = { username: session.username };
|
||||
req.token = token;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error en autenticación:', error);
|
||||
@@ -63,3 +64,73 @@ export async function basicAuthMiddleware(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
// Alias para mantener compatibilidad
|
||||
export const basicAuthMiddleware = authMiddleware;
|
||||
|
||||
// Función para crear sesión
|
||||
export async function createSession(username) {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
throw new Error('Redis no está disponible');
|
||||
}
|
||||
|
||||
const token = generateToken();
|
||||
const sessionKey = `session:${token}`;
|
||||
const sessionData = {
|
||||
username,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Almacenar sesión en Redis con TTL
|
||||
await redisClient.setEx(sessionKey, SESSION_DURATION, JSON.stringify(sessionData));
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// Función para invalidar sesión
|
||||
export async function invalidateSession(token) {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionKey = `session:${token}`;
|
||||
await redisClient.del(sessionKey);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error invalidando sesión:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Función para invalidar todas las sesiones de un usuario
|
||||
export async function invalidateUserSessions(username) {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Buscar todas las sesiones del usuario
|
||||
const keys = await redisClient.keys('session:*');
|
||||
let count = 0;
|
||||
|
||||
for (const key of keys) {
|
||||
const sessionData = await redisClient.get(key);
|
||||
if (sessionData) {
|
||||
const session = JSON.parse(sessionData);
|
||||
if (session.username === username) {
|
||||
await redisClient.del(key);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
console.error('Error invalidando sesiones del usuario:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,93 @@
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { getRedisClient } from '../services/redis.js';
|
||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
||||
import { basicAuthMiddleware, createSession, invalidateSession, invalidateUserSessions } from '../middlewares/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Endpoint de login (público)
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const redisClient = getRedisClient();
|
||||
if (!redisClient) {
|
||||
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'username y password son requeridos' });
|
||||
}
|
||||
|
||||
// Buscar usuario en Redis
|
||||
const userKey = `user:${username}`;
|
||||
const userExists = await redisClient.exists(userKey);
|
||||
|
||||
if (!userExists) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||
}
|
||||
|
||||
// Obtener hash de la contraseña
|
||||
const userData = await redisClient.hGetAll(userKey);
|
||||
const passwordHash = userData.passwordHash;
|
||||
|
||||
if (!passwordHash) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||
}
|
||||
|
||||
// Verificar contraseña
|
||||
const match = await bcrypt.compare(password, passwordHash);
|
||||
|
||||
if (!match) {
|
||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||
}
|
||||
|
||||
// Crear sesión/token
|
||||
const token = await createSession(username);
|
||||
|
||||
console.log(`✅ Login exitoso: ${username}`);
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
username,
|
||||
message: 'Login exitoso'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error en login:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint de logout (requiere autenticación)
|
||||
router.post('/logout', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
const token = req.token;
|
||||
|
||||
if (token) {
|
||||
await invalidateSession(token);
|
||||
console.log(`✅ Logout exitoso: ${req.user.username}`);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Logout exitoso' });
|
||||
} catch (error) {
|
||||
console.error('Error en logout:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Verificar token (para validar si la sesión sigue activa)
|
||||
router.get('/me', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
username: req.user.username,
|
||||
authenticated: true
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Cambiar contraseña de usuario
|
||||
router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
||||
try {
|
||||
@@ -45,8 +128,11 @@ router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
console.log(`✅ Contraseña actualizada para usuario: ${username}`);
|
||||
res.json({ success: true, message: 'Contraseña actualizada correctamente' });
|
||||
// Invalidar todas las sesiones del usuario (requiere nuevo login)
|
||||
await invalidateUserSessions(username);
|
||||
|
||||
console.log(`✅ Contraseña actualizada para usuario: ${username} (todas las sesiones invalidadas)`);
|
||||
res.json({ success: true, message: 'Contraseña actualizada correctamente. Por favor, inicia sesión nuevamente.' });
|
||||
} catch (error) {
|
||||
console.error('Error cambiando contraseña:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
Reference in New Issue
Block a user