535 lines
12 KiB
Markdown
535 lines
12 KiB
Markdown
# 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:**
|
|
```python
|
|
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:**
|
|
```python
|
|
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
|
|
|
|
```python
|
|
def _generate_signup_token(self):
|
|
""" Generar token aleatorio único """
|
|
return secrets.token_urlsafe(32) # 32 bytes = 256 bits
|
|
```
|
|
|
|
**Aplicabilidad MGN-001:** ⭐⭐⭐⭐⭐
|
|
```typescript
|
|
// Node.js equivalent
|
|
import crypto from 'crypto';
|
|
|
|
function generateToken(): string {
|
|
return crypto.randomBytes(32).toString('base64url');
|
|
}
|
|
```
|
|
|
|
### 2. Expiración de Tokens
|
|
|
|
```python
|
|
@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
|
|
|
|
```python
|
|
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:**
|
|
```typescript
|
|
// 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
|
|
|
|
```python
|
|
_sql_constraints = [
|
|
('login_unique', 'UNIQUE (login)', 'Email already exists')
|
|
]
|
|
```
|
|
|
|
### 3. Token Válido
|
|
|
|
```python
|
|
if not user.signup_valid:
|
|
raise AccessDenied('Token expired or invalid')
|
|
```
|
|
|
|
---
|
|
|
|
## Seguridad
|
|
|
|
### 1. Protección contra Timing Attacks
|
|
|
|
```python
|
|
# 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:** ⭐⭐⭐⭐⭐
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
// 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
|
|
|
|
```html
|
|
<!-- 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
|
|
|
|
```html
|
|
<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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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
|
|
|
|
```sql
|
|
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
|
|
|
|
```sql
|
|
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):**
|
|
```typescript
|
|
// 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):**
|
|
```typescript
|
|
// Usuario activo inmediatamente
|
|
await createUser({
|
|
email,
|
|
password,
|
|
name,
|
|
email_verified: false,
|
|
active: true // Activo de inmediato
|
|
});
|
|
```
|
|
|
|
### 2. Proceso de Reset Password
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|