372 lines
8.2 KiB
Markdown
372 lines
8.2 KiB
Markdown
# 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
|