Autenticacion JWT y RBAC¶
GexCom implementa autenticacion basada en JWT (JSON Web Tokens) con control de acceso basado en roles (RBAC) y proteccion contra fuerza bruta con lockout persistente.
Flujo de Autenticacion¶
sequenceDiagram
actor Usuario
participant GUI as Streamlit / API
participant AS as AuthService
participant LAS as SqlLoginAttemptStore
participant DB as PostgreSQL
Usuario->>GUI: email + password
GUI->>AS: login(email, password)
AS->>LAS: get_attempt(email)
LAS->>DB: SELECT login_attempt
DB-->>LAS: LoginAttempt | None
alt Cuenta bloqueada (TTL activo)
AS-->>GUI: Failure("Cuenta bloqueada")
else No bloqueada
AS->>DB: SELECT usuario WHERE email=?
DB-->>AS: Usuario
alt Password correcto (bcrypt.verify)
AS->>LAS: reset_attempts(email)
AS->>AS: jwt.encode({sub, rol, exp})
AS-->>GUI: Success(LoginResponse{token})
else Password incorrecto
AS->>LAS: increment_attempts(email)
alt intentos >= 5
AS->>LAS: set_lockout(email, TTL=15min)
end
AS-->>GUI: Failure("Credenciales invalidas")
end
end
JWT — Estructura del Token¶
| Campo | Descripcion |
|---|---|
sub |
UUID del usuario autenticado |
rol |
ADMINISTRADOR o NOTIFICADOR |
exp |
Timestamp de expiracion (480 min desde emision) |
Algoritmo: HS256
Expiracion: 480 minutos (configurable via JWT_EXPIRATION_MINUTES)
Validacion del Token (API)¶
# src/gexcom/interfaces/api/dependencies.py
def get_current_user(credentials: HTTPAuthorizationCredentials) -> CurrentUser:
payload = jwt.decode(credentials.credentials, settings.secret_key, algorithms=[settings.jwt_algorithm])
user_id = payload.get("sub")
rol = payload.get("rol")
if user_id is None or rol is None:
raise HTTPException(401, "Token invalido: claims faltantes")
return CurrentUser(usuario_id=UUID(user_id), rol=RolUsuario(rol))
RBAC — Control de Acceso por Roles¶
Roles del Sistema¶
| Rol | Descripcion | Permisos |
|---|---|---|
ADMINISTRADOR |
Acceso total | CRUD completo, usuarios, reportes, config |
NOTIFICADOR |
Acceso operativo | Procesos, sujetos, notificaciones, audiencias |
Verificacion en GUI (Streamlit)¶
Cada pagina protegida llama a check_permission(user, accion):
# src/gexcom/interfaces/gui/auth_guard.py
def check_permission(user: UsuarioSesion, accion: str) -> bool:
return container.authorization_service.can(user.rol, accion)
Si el usuario no tiene permiso, se muestra un mensaje de error y se detiene el renderizado con st.stop().
Verificacion en API (FastAPI)¶
# Endpoint solo para ADMINISTRADOR
@router.get("/usuarios")
def list_usuarios(
user: Annotated[CurrentUser, Depends(require_admin)],
container: Annotated[ServiceContainer, Depends(get_container)],
) -> list[UsuarioResponse]:
...
# require_admin en dependencies.py
def require_admin(user: Annotated[CurrentUser, Depends(get_current_user)]) -> CurrentUser:
if user.rol != RolUsuario.ADMINISTRADOR:
raise HTTPException(403, "Se requiere rol ADMINISTRADOR")
return user
Lockout Persistente¶
Configuracion¶
| Parametro | Valor |
|---|---|
| Intentos maximos | 5 fallidos consecutivos |
| Duracion lockout | 15 minutos (TTL) |
| Persistencia | Base de datos (login_attempts tabla) |
| Reset | Automatico al autenticarse correctamente |
Por que Persistente?¶
A diferencia de lockouts en memoria, el lockout de GexCom sobrevive a reinicios del servidor y aplica correctamente en ambientes multi-proceso (Streamlit + API corriendo simultaneamente).
# src/gexcom/infrastructure/security/login_attempt_store.py
class SqlLoginAttemptStore:
def is_locked(self, email: str) -> bool:
attempt = self._get(email)
if attempt is None:
return False
if attempt.bloqueado_hasta and datetime.utcnow() < attempt.bloqueado_hasta:
return True
return False
Hasher de Contrasenas¶
GexCom usa bcrypt via passlib:
# src/gexcom/infrastructure/security/password_hasher.py
class BcryptPasswordHasher:
def hash(self, plain: str) -> str:
return bcrypt.hash(plain)
def verify(self, plain: str, hashed: str) -> bool:
return bcrypt.verify(plain, hashed)
Las contrasenas nunca se almacenan en texto plano. Solo se guarda el hash bcrypt.
Backlog P03 — Mejoras Pendientes¶
Items en backlog
Los siguientes items de seguridad estan documentados para P03:
| ID | Hallazgo | Prioridad |
|---|---|---|
API-RATE |
Sin rate limiting HTTP en /auth/login |
HIGH |
API-RBAC |
Sin RBAC granular en endpoints API (solo admin/no-admin) | HIGH |
API-HEADERS |
Sin security headers HTTP (HSTS, CSP, X-Frame-Options) | MEDIUM |
API-CORS |
CORS allow_origins=["*"] debe restringirse en produccion |
MEDIUM |