🌱

CultivaHub — Arquitectura de Autenticación y Autorización

Referencia técnica para el equipo de desarrollo · Stack: Node.js + Express + TypeScript + PostgreSQL + Redis

JWT + OAuth2 RBAC v1.0 · 2026-06-16
🔑

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/callback
Registrar en Google Cloud Console como URI autorizado
Configurar
👤
Find-or-Create
Si el usuario no existe se crea automáticamente con rol viewer por defecto
Seguro
🍪
Token en Cookie
El JWT se devuelve en httpOnly cookie, nunca en la URL de redirección
Recomendado
🏢
Multi-workspace
El payload incluye workspaceId para aislar datos entre agencias clientes
SaaS
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=strict
HTTPS 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.