JWT con Refresh Token Flow
Access token de 15 min · Refresh token de 7 días · almacenado en httpOnly cookie
Flujo completo de autenticación
01
Login
email + pass
→
02
Verificar
bcrypt hash
→
03
Generar JWT
access + refresh
→
04
Cookie
httpOnly secure
→
05
Petición API
Bearer token
→
06
Middleware
verify + attach
→
07
Respuesta
recurso seguro
src/auth/jwt.service.ts
TypeScript
// ─── CultivaHub · JWT Service ──────────────────────────────────── import jwt from 'jsonwebtoken'; interface JWTPayload { userId: string; email: string; role: 'admin' | 'manager' | 'viewer'; workspaceId: string; // multitenancy de CultivaHub } /** Genera par access + refresh token */ export function generateTokens(payload: JWTPayload) { const accessToken = jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '15m', // corto · renovar con refresh }); const refreshToken = jwt.sign( { userId: payload.userId }, process.env.JWT_REFRESH_SECRET!, { expiresIn: '7d' } ); return { accessToken, refreshToken }; } /** Middleware: valida Bearer token y adjunta req.user */ export function authenticate(req, res, next) { const token = req.headers.authorization?.substring(7); if (!token) return res.status(401).json({ error: 'Sin token' }); try { req.user = jwt.verify(token, process.env.JWT_SECRET!); next(); } catch { res.status(401).json({ error: 'Token inválido o expirado' }); } }
OAuth2 — Login con Google (Passport.js)
Find-or-create de usuario · redirección con JWT · sin contraseña almacenada
Callback URL
/api/auth/google/callbackRegistrar en Google Cloud Console como URI autorizado
Find-or-Create
Si el usuario no existe se crea automáticamente con rol
Seguro
viewer por defectoToken en Cookie
El JWT se devuelve en httpOnly cookie, nunca en la URL de redirección
Recomendado
Multi-workspace
El payload incluye
SaaS
workspaceId para aislar datos entre agencias clientes
src/auth/google.strategy.ts
TypeScript
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; passport.use(new GoogleStrategy({ clientID: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, callbackURL: '/api/auth/google/callback', }, async (_, __, profile, done) => { let user = await db.users.findOne({ googleId: profile.id }); if (!user) { user = await db.users.create({ googleId: profile.id, email: profile.emails?.[0]?.value, name: profile.displayName, role: 'viewer', // rol mínimo por defecto workspaceId: await resolveWorkspace(profile.emails?.[0]?.value), }); } return done(null, user); })); // Ruta de callback — emite JWT en cookie httpOnly app.get('/api/auth/google/callback', passport.authenticate('google', { session: false }), (req, res) => { const { accessToken, refreshToken } = generateTokens(req.user); res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 604800000 }); res.redirect(`${process.env.FRONTEND_URL}/dashboard?token=${accessToken}`); } );
RBAC — Matriz de Permisos CultivaHub
Tres roles · jerarquía heredada · middleware reutilizable
🔐 Matriz de permisos por recurso
| Recurso / Acción | Admin | Manager | Viewer |
|---|---|---|---|
| Gestionar workspaces (crear/eliminar) | ✓ | — | — |
| Invitar / eliminar usuarios del equipo | ✓ | — | — |
| Ver todos los usuarios | ✓ | ✓ | — |
| Crear / editar campañas | ✓ | ✓ | — |
| Editar campañas propias | ✓ | ✓ | — |
| Ver campañas (lectura) | ✓ | ✓ | ✓ |
| Ver reportes y analytics | ✓ | ✓ | ✓ |
| Exportar datos (CSV/PDF) | ✓ | ✓ | — |
| Configurar integraciones (IA, webhooks) | ✓ | — | — |
| Ver dashboard de uso de la plataforma | ✓ | ✓ | ✓ |
src/auth/rbac.middleware.ts
TypeScript
enum Role { ADMIN = 'admin', MANAGER = 'manager', VIEWER = 'viewer' } // Jerarquía: admin hereda todos los permisos de manager y viewer const roleHierarchy: Record<Role, Role[]> = { [Role.ADMIN]: [Role.ADMIN, Role.MANAGER, Role.VIEWER], [Role.MANAGER]: [Role.MANAGER, Role.VIEWER], [Role.VIEWER]: [Role.VIEWER], }; export function requireRole(...roles: Role[]) { return (req, res, next) => { if (!req.user) return res.status(401).json({ error: 'No autenticado' }); const allowed = roles.some(r => roleHierarchy[req.user.role]?.includes(r)); if (!allowed) return res.status(403).json({ error: 'Permisos insuficientes' }); next(); }; } // Ejemplos de uso en las rutas de CultivaHub router.delete('/workspaces/:id', authenticate, requireRole(Role.ADMIN), deleteWorkspace); router.post('/campaigns', authenticate, requireRole(Role.MANAGER), createCampaign); router.get('/campaigns', authenticate, requireRole(Role.VIEWER), listCampaigns);
Checklist de Seguridad — Pre-Deploy
Verificar antes de subir a producción
Nunca almacenar JWT en localStorage
CultivaHub usa httpOnly cookies. localStorage es vulnerable a XSS. Asegurarse que el frontend no guarda tokens en window.localStorage ni window.sessionStorage.
🔒 Security Checklist — CultivaHub Auth
Contraseñas y hashing
bcrypt con salt rounds = 12
Mínimo 12 para producción; 10 solo en tests.
Validación de contraseña: mínimo 12 chars, mayúscula, número, carácter especial
Reset de contraseña con token firmado y TTL de 1 hora
Tokens JWT
Access token: 15 min · Refresh token: 7 días
Refresh tokens hasheados en BD antes de almacenar
Revocación de todos los tokens al detectar compromiso (logout global)
Rotar JWT_SECRET y JWT_REFRESH_SECRET cada 90 días
Programar rotación con GitHub Actions o equivalente.
Cookies y transporte
Cookies con flags:
httpOnly · secure · sameSite=strictHTTPS obligatorio en producción (HSTS habilitado)
CORS configurado con whitelist de orígenes, no wildcard *
Rate limiting y logs
Login: máx 5 intentos / 15 min por IP (Redis store)
API general: máx 100 req/min por usuario autenticado
Logs de eventos de seguridad: logins fallidos, cambios de rol, revocaciones
MFA (TOTP) pendiente de implementar para cuentas Admin
Usar otplib + QR code en próxima iteración.