JSON WEBTOKENS!!
This commit is contained in:
371
JWT_AUTHENTICATION.md
Normal file
371
JWT_AUTHENTICATION.md
Normal 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
|
||||
Reference in New Issue
Block a user