erp-core/docs/01-analisis-referencias/odoo/odoo-auth-analysis.md

12 KiB

Análisis del Módulo auth_signup de Odoo

Módulo: auth_signup Versión Odoo: 18.0 Community Prioridad: P0 (Crítico) Mapeo MGN: MGN-001 (Fundamentos - Autenticación)


Descripción

Módulo que extiende base para agregar funcionalidades de:

  • Registro de usuarios (signup)
  • Reseteo de contraseña
  • Invitación de usuarios
  • Tokens de verificación

Modelos Extendidos

res.users (extensión)

Campos añadidos:

signup_token = fields.Char(copy=False)                # Token único para signup
signup_type = fields.Selection([
    ('signup', 'Signup'),
    ('reset', 'Reset Password')
])
signup_expiration = fields.Datetime()
signup_valid = fields.Boolean(compute='_compute_signup_valid')

Métodos clave:

def signup(self, values, token=None):
    """ Proceso de registro de usuario """
    # 1. Validar token
    # 2. Crear usuario
    # 3. Invalidar token
    # 4. Enviar email de bienvenida

def reset_password(self, login):
    """ Generar token para reset de password """
    # 1. Buscar usuario por email
    # 2. Generar token único
    # 3. Configurar expiración (24h)
    # 4. Enviar email con link

def change_password(self, old_passwd, new_passwd):
    """ Cambio de contraseña """
    # 1. Validar contraseña actual
    # 2. Validar nueva contraseña (complejidad)
    # 3. Actualizar hash
    # 4. Invalidar sesiones activas

Lógica de Negocio Destacable

1. Generación de Tokens Seguros

def _generate_signup_token(self):
    """ Generar token aleatorio único """
    return secrets.token_urlsafe(32)  # 32 bytes = 256 bits

Aplicabilidad MGN-001:

// Node.js equivalent
import crypto from 'crypto';

function generateToken(): string {
  return crypto.randomBytes(32).toString('base64url');
}

2. Expiración de Tokens

@api.depends('signup_token', 'signup_expiration')
def _compute_signup_valid(self):
    for user in self:
        if user.signup_token and user.signup_expiration:
            user.signup_valid = datetime.now() <= user.signup_expiration
        else:
            user.signup_valid = False

Aplicabilidad MGN-001:

  • Tokens expiran en 24-48 horas
  • Validación automática antes de usar

3. Proceso de Signup

def signup(self, values, token=None):
    # Validar token
    if token:
        user = self.search([('signup_token', '=', token), ('signup_valid', '=', True)])
        if not user:
            raise SignupError('Invalid token')

    # Crear usuario
    user = self.create({
        'login': values['email'],
        'password': values['password'],
        'name': values['name'],
        'groups_id': [(6, 0, [self.env.ref('base.group_user').id])]
    })

    # Invalidar token
    if token:
        user.signup_token = False

    # Enviar email bienvenida
    user._send_welcome_email()

    return user

Flujos de Usuario

Flujo 1: Registro de Usuario (Signup)

1. Usuario visita /signup
2. Completa formulario (nombre, email, password)
3. POST /web/signup
4. Backend:
   a. Valida datos (email único, password fuerte)
   b. Crea usuario con estado "pendiente" o "activo"
   c. Envía email de verificación (opcional)
5. Usuario recibe email
6. Click en link de verificación
7. GET /web/signup/verify?token=XXX
8. Backend activa usuario
9. Redirect a /login

Flujo 2: Reset de Contraseña

1. Usuario visita /reset
2. Ingresa email
3. POST /web/reset_password
4. Backend:
   a. Busca usuario por email
   b. Genera token único
   c. Configura expiración (24h)
   d. Envía email con link
5. Usuario recibe email
6. Click en link
7. GET /web/reset_password?token=XXX
8. Muestra formulario de nueva contraseña
9. POST /web/reset_password/confirm
10. Backend:
    a. Valida token
    b. Actualiza password
    c. Invalida token
11. Redirect a /login

Validaciones

1. Complejidad de Contraseña

Odoo no implementa validación de complejidad por defecto, pero es buena práctica.

Recomendación MGN-001:

// Validación de password fuerte
const passwordSchema = z.string()
  .min(8, 'Mínimo 8 caracteres')
  .regex(/[A-Z]/, 'Debe contener mayúscula')
  .regex(/[a-z]/, 'Debe contener minúscula')
  .regex(/[0-9]/, 'Debe contener número')
  .regex(/[^A-Za-z0-9]/, 'Debe contener carácter especial');

2. Email Único

_sql_constraints = [
    ('login_unique', 'UNIQUE (login)', 'Email already exists')
]

3. Token Válido

if not user.signup_valid:
    raise AccessDenied('Token expired or invalid')

Seguridad

1. Protección contra Timing Attacks

# Odoo NO implementa esto, pero debería
import secrets

def verify_token(token_from_user, token_in_db):
    # secrets.compare_digest es resistente a timing attacks
    return secrets.compare_digest(token_from_user, token_in_db)

Aplicabilidad MGN-001:

import { timingSafeEqual } from 'crypto';

function verifyToken(tokenFromUser: string, tokenInDB: string): boolean {
  const bufA = Buffer.from(tokenFromUser);
  const bufB = Buffer.from(tokenInDB);
  if (bufA.length !== bufB.length) return false;
  return timingSafeEqual(bufA, bufB);
}

2. Rate Limiting

Odoo NO implementa rate limiting para signup/reset.

Recomendación MGN-001:

// Usar express-rate-limit
import rateLimit from 'express-rate-limit';

const signupLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 5, // 5 intentos por IP
  message: 'Demasiados intentos, intente más tarde'
});

app.post('/api/v1/auth/signup', signupLimiter, signupController);

3. CAPTCHA

Odoo soporta reCAPTCHA (opcional).

Recomendación MGN-001: Implementar Google reCAPTCHA v3


Emails Transaccionales

1. Email de Verificación

<!-- Template Odoo -->
<p>Hola ${object.name},</p>
<p>Gracias por registrarte en ${object.company_id.name}.</p>
<p>Para completar tu registro, haz click en el siguiente enlace:</p>
<a href="${object.signup_url}">Verificar mi cuenta</a>
<p>Este enlace expira en 24 horas.</p>

2. Email de Reset de Contraseña

<p>Hola ${object.name},</p>
<p>Has solicitado restablecer tu contraseña.</p>
<p>Haz click en el siguiente enlace para crear una nueva contraseña:</p>
<a href="${object.reset_url}">Restablecer contraseña</a>
<p>Si no solicitaste este cambio, ignora este email.</p>
<p>Este enlace expira en 24 horas.</p>

Aplicabilidad MGN-001:

  • Usar servicio de emails transaccionales (SendGrid, AWS SES)
  • Templates con Handlebars o React Email

Mapeo a MGN-001

Requerimientos Funcionales

RF-AUTH-005: Registro de usuarios

  • Endpoint: POST /api/v1/auth/signup
  • Validaciones: email único, password fuerte
  • Email de verificación opcional
  • Activación automática o manual

RF-AUTH-006: Reset de contraseña

  • Endpoint: POST /api/v1/auth/reset-password
  • Generación de token seguro
  • Expiración 24 horas
  • Email con link de reset

RF-AUTH-007: Cambio de contraseña

  • Endpoint: PUT /api/v1/auth/change-password
  • Requiere autenticación
  • Validar contraseña actual
  • Validar nueva contraseña

RF-AUTH-008: Verificación de email

  • Endpoint: GET /api/v1/auth/verify?token=XXX
  • Validar token
  • Activar usuario
  • Invalidar token

Especificaciones Técnicas

ET-AUTH-005-backend: API de Registro

// POST /api/v1/auth/signup
interface SignupRequest {
  name: string;
  email: string;
  password: string;
  recaptchaToken?: string;
}

interface SignupResponse {
  message: string;
  requiresVerification: boolean;
}

async function signup(req: SignupRequest): Promise<SignupResponse> {
  // 1. Validar reCAPTCHA
  // 2. Validar datos (Zod schema)
  // 3. Verificar email único
  // 4. Hash password (bcrypt)
  // 5. Crear usuario
  // 6. Generar token de verificación
  // 7. Enviar email
  // 8. Return response
}

ET-AUTH-006-backend: API de Reset Password

// POST /api/v1/auth/reset-password
interface ResetPasswordRequest {
  email: string;
}

interface ResetPasswordResponse {
  message: string;
}

async function resetPassword(req: ResetPasswordRequest): Promise<ResetPasswordResponse> {
  // 1. Buscar usuario por email
  // 2. Generar token (crypto.randomBytes)
  // 3. Guardar token + expiration (24h)
  // 4. Enviar email con link
  // 5. Return response genérico (no revelar si email existe)
}

Implementación Recomendada

Tabla auth.password_reset_tokens

CREATE TABLE auth.password_reset_tokens (
  id SERIAL PRIMARY KEY,
  user_id INT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  token VARCHAR(100) NOT NULL UNIQUE,
  expires_at TIMESTAMPTZ NOT NULL,
  used BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_password_reset_tokens_token ON auth.password_reset_tokens(token)
WHERE used = FALSE AND expires_at > NOW();

Tabla auth.email_verifications

CREATE TABLE auth.email_verifications (
  id SERIAL PRIMARY KEY,
  user_id INT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  token VARCHAR(100) NOT NULL UNIQUE,
  expires_at TIMESTAMPTZ NOT NULL,
  verified BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_email_verifications_token ON auth.email_verifications(token)
WHERE verified = FALSE AND expires_at > NOW();

Recomendaciones de Implementación

1. Proceso de Signup

Con verificación de email (recomendado):

// 1. Usuario se registra
await createUser({
  email,
  password,
  name,
  email_verified: false,
  active: false  // Inactivo hasta verificar
});

// 2. Generar token
const token = crypto.randomBytes(32).toString('base64url');
await createEmailVerification({
  user_id,
  token,
  expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24h
});

// 3. Enviar email
await sendEmail({
  to: email,
  subject: 'Verifica tu cuenta',
  template: 'email-verification',
  data: {
    name,
    verificationUrl: `${APP_URL}/auth/verify?token=${token}`
  }
});

Sin verificación de email (menos seguro):

// Usuario activo inmediatamente
await createUser({
  email,
  password,
  name,
  email_verified: false,
  active: true  // Activo de inmediato
});

2. Proceso de Reset Password

// 1. Usuario solicita reset
const user = await findUserByEmail(email);
if (!user) {
  // NO revelar que el email no existe (seguridad)
  return { message: 'Si el email existe, recibirás instrucciones' };
}

// 2. Generar token
const token = crypto.randomBytes(32).toString('base64url');
await createPasswordResetToken({
  user_id: user.id,
  token,
  expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000)
});

// 3. Enviar email
await sendEmail({
  to: email,
  subject: 'Restablece tu contraseña',
  template: 'password-reset',
  data: {
    name: user.name,
    resetUrl: `${APP_URL}/auth/reset?token=${token}`
  }
});

return { message: 'Si el email existe, recibirás instrucciones' };

3. Validación de Token

async function verifyEmailToken(token: string): Promise<void> {
  const verification = await findEmailVerification(token);

  if (!verification) {
    throw new Error('Token inválido');
  }

  if (verification.verified) {
    throw new Error('Token ya utilizado');
  }

  if (verification.expires_at < new Date()) {
    throw new Error('Token expirado');
  }

  // Activar usuario
  await updateUser(verification.user_id, {
    email_verified: true,
    active: true
  });

  // Marcar token como usado
  await markVerificationAsUsed(verification.id);
}

Conclusión

El módulo auth_signup de Odoo proporciona funcionalidades esenciales de autenticación que deben implementarse en MGN-001.

Funcionalidades clave a implementar:

  1. Registro de usuarios con validación
  2. Verificación de email (token seguro)
  3. Reset de contraseña (token con expiración)
  4. Cambio de contraseña (autenticado)

Mejoras vs Odoo:

  1. Validación de complejidad de contraseña
  2. Rate limiting en endpoints sensibles
  3. reCAPTCHA en signup/reset
  4. Timing-safe token comparison
  5. JWT en lugar de sesiones

Nivel de reutilización: 85% de patrones aplicables


Fecha: 2025-11-23 Analista: Architecture-Analyst Estado: Análisis completo