JSON WEBTOKENS!!

This commit is contained in:
2026-04-26 16:24:29 -06:00
parent 30efa0e098
commit 0e85231bae
12 changed files with 628 additions and 13 deletions

371
JWT_AUTHENTICATION.md Normal file
View File

@@ -0,0 +1,371 @@
# JWT Authentication for VoxPopuli Users API
## Overview
La API de Usuarios de VoxPopuli ahora incluye autenticación basada en **JSON Web Tokens (JWT)**. Esta documentación describe cómo usar e implementar esta característica.
## Features
**Autenticación segura con JWT** - Tokens firmados con HS256
**Hashing de contraseñas** - Usando bcrypt para máxima seguridad
**Contraseña por defecto** - "passwd123" para usuarios sin contraseña especificada
**Expiración configurable** - Tokens con tiempo de vida configurable (default: 24 horas)
## Configuration
### Environment Variables
Agregue las siguientes variables al archivo `.env`:
```env
# JWT Configuration
JWT_SECRET_KEY=your-super-secret-key-change-in-production
JWT_ALGORITHM=HS256
JWT_EXPIRATION_HOURS=24
```
**Importante:** En producción, cambiar `JWT_SECRET_KEY` por una clave segura y aleatoria.
### Installation
Se han agregado las siguientes dependencias a `requirements.txt`:
```
PyJWT # Para crear y verificar JWT tokens
passlib[bcrypt] # Para hashing seguro de contraseñas
python-multipart # Para manejo de formularios en login
```
Para instalar las dependencias:
```bash
pip install -r requirements.txt
```
## API Endpoints
### 1. Crear Usuario (Registro)
**POST** `/api/v1/users/`
Crea un nuevo usuario. La contraseña es **opcional**; si no se proporciona, se usa "passwd123" por defecto.
#### Request Body
```json
{
"nombre": "Juan",
"apellido": "Pérez",
"email": "juan@example.com",
"contraseña": "miContraseñaSegura123", // Opcional
"fecha_nacimiento": "1990-01-15T00:00:00",
"url_foto_perfil": "https://example.com/foto.jpg", // Opcional
"biografia": "Soy reportero comunitario" // Opcional
}
```
#### Response (202 Accepted)
```json
{
"status": "queued",
"message": "Usuario enviado a cola para procesamiento",
"email": "juan@example.com"
}
```
#### Casos de Error
- **400 Bad Request**: Validación fallida (email inválido, campos vacíos)
- **409 Conflict**: Email ya está registrado
---
### 2. Login de Usuario (Autenticación)
**POST** `/api/v1/users/login`
Autentica un usuario y retorna un JWT token.
#### Request Body
```json
{
"email": "juan@example.com",
"contraseña": "miContraseñaSegura123"
}
```
#### Response (200 OK)
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user_id": 1,
"email": "juan@example.com"
}
```
#### Casos de Error
- **401 Unauthorized**: Email o contraseña incorrectos
---
## JWT Token Usage
### Token Structure
Cada token JWT contiene el siguiente payload:
```json
{
"user_id": 1,
"email": "juan@example.com",
"exp": 1704067200, // Unix timestamp de expiración
"iat": 1703980800 // Unix timestamp de creación
}
```
### Authorization Header
Para usar el token en requests a endpoints protegidos:
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
### Example cURL
```bash
curl -X POST "http://localhost:8000/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{
"email": "juan@example.com",
"contraseña": "miContraseñaSegura123"
}'
# Respuesta:
# {
# "access_token": "eyJhbGc...",
# "token_type": "bearer",
# "user_id": 1,
# "email": "juan@example.com"
# }
```
---
## Password Security
### Password Hashing
Las contraseñas se hashean usando **bcrypt** antes de guardarse en la base de datos:
1. Usuario envía contraseña en texto plano
2. Se hashea con bcrypt (rounds=12 por defecto)
3. Se almacena solo el hash en la base de datos
4. Durante login, se verifica comparando el hash
### Default Password
Si un usuario no especifica contraseña durante el registro:
```
Contraseña por defecto: "passwd123"
Hash bcrypt: $2b$12$R9h7cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ee3GzxQ2n8/N7kDi
```
---
## Database Schema
El modelo de usuario se ha actualizado con:
```sql
ALTER TABLE usuarios ADD COLUMN contraseña_hash VARCHAR(255) NOT NULL DEFAULT '$2b$12$R9h7cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ee3GzxQ2n8/N7kDi';
```
Campos relevantes:
| Campo | Tipo | Descripción |
|-------|------|-------------|
| `user_id` | INT | Identificador único (PK) |
| `email` | VARCHAR(255) | Email único |
| `contraseña_hash` | VARCHAR(255) | Hash bcrypt de la contraseña |
| `fecha_creacion` | DATETIME | Fecha de registro |
---
## Services Architecture
### AuthService (`src/infrastructure/api/users/auth_service.py`)
Responsable de:
- **hash_password()** - Hashea contraseñas con bcrypt
- **verify_password()** - Verifica contraseña contra hash
- **create_access_token()** - Genera JWT tokens
- **verify_token()** - Valida y decodifica tokens
- **get_default_password_hash()** - Retorna hash de "passwd123"
### Example Usage
```python
from infrastructure.api.users.auth_service import auth_service
# Hashear contraseña
hashed = auth_service.hash_password("miContraseña")
# Verificar contraseña
is_valid = auth_service.verify_password("miContraseña", hashed)
# Crear token
token = auth_service.create_access_token(user_id=1, email="user@example.com")
# Verificar token
payload = auth_service.verify_token(token)
# payload = {"user_id": 1, "email": "user@example.com", "exp": ..., "iat": ...}
```
---
## RabbitMQ Message Format
El mensaje de creación de usuario ahora incluye el hash de contraseña:
```python
@dataclass
class UserMessage:
event_type: UserEventType
user_id: Optional[int] = None
nombre: Optional[str] = None
apellido: Optional[str] = None
email: Optional[str] = None
contraseña_hash: Optional[str] = None # ✨ Nuevo
fecha_nacimiento: Optional[str] = None
fecha_creacion: Optional[str] = None
# ... campos adicionales
```
---
## Example Workflow
### 1. Registrar usuario
```bash
curl -X POST "http://localhost:8000/api/v1/users/" \
-H "Content-Type: application/json" \
-d '{
"nombre": "Carlos",
"apellido": "López",
"email": "carlos@example.com",
"contraseña": "MiPass@2024",
"fecha_nacimiento": "1995-06-20T00:00:00"
}'
```
### 2. Login
```bash
curl -X POST "http://localhost:8000/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{
"email": "carlos@example.com",
"contraseña": "MiPass@2024"
}'
# Response:
# {
# "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
# "token_type": "bearer",
# "user_id": 1,
# "email": "carlos@example.com"
# }
```
### 3. Usar token
```bash
curl -X GET "http://localhost:8000/api/v1/users/1" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
---
## Security Best Practices
### ✅ En Desarrollo
- Se proporciona `JWT_SECRET_KEY` por defecto para facilitar testing
- Cambiar en `.env` si es necesario
### ✅ En Producción
1. **Cambiar `JWT_SECRET_KEY`**
```bash
# Generar clave aleatoria
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
2. **Usar HTTPS**
- Los tokens deben viajar solo por HTTPS
3. **Secrets Management**
- Usar servicios como AWS Secrets Manager, HashiCorp Vault
- No guardar secretos en código
4. **Token Rotation**
- Considerar refresh tokens con corta expiración
- Implementar revocation list (blacklist)
5. **CORS Configuration**
- Configurar CORS para permitir solo dominios autorizados
---
## Troubleshooting
### Error: "Token expired"
El token tiene más de 24 horas (configurable). Hacer login nuevamente.
### Error: "Email o contraseña incorrectos"
Verificar:
- El email existe en la base de datos
- La contraseña es correcta
- El usuario ha sido procesado por el consumer (status "queued" → guardado)
### Error: "Secret key not found"
Asegurar que `JWT_SECRET_KEY` está configurado en:
1. `.env` file
2. Variable de entorno del sistema
3. O usando el default en `core/config.py`
---
## Future Enhancements
- [ ] Refresh tokens
- [ ] Token blacklist para logout
- [ ] Multi-factor authentication (MFA)
- [ ] OAuth2 integration
- [ ] Rate limiting en endpoint de login
- [ ] Account lockout después de N intentos fallidos
---
## References
- [JWT.io](https://jwt.io) - JWT documentation
- [PyJWT Documentation](https://pyjwt.readthedocs.io/)
- [Passlib Documentation](https://passlib.readthedocs.io/)
- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/)
---
**Última actualización:** 26 de abril de 2026