# 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