Autenticación y roles en una app interna paso a paso
Muchas apps internas empiezan sin seguridad real.
Primero son “solo para el equipo”. Después “solo para dos personas”. Después alguien comparte una URL. Después aparece un acceso improvisado.
Y cuando por fin decides poner autenticación, el sistema ya asumía que cualquiera podía hacer cualquier cosa.
Este tutorial está pensado para evitar ese camino.
Qué vas a construir
La base conceptual será esta:
Usuario
↓
Login
↓
Token o sesión
↓
Endpoints protegidos
↓
Chequeo de rol
↓
Acción permitida o denegada
No vamos a cubrir todos los modelos posibles de auth.
Vamos a dejar una base razonable para una app interna típica.
Cuándo conviene este enfoque
Tiene sentido cuando:
- varias personas usan la app
- no todos deberían ver lo mismo
- algunas acciones tienen impacto operativo real
No conviene seguir postergándolo cuando:
- ya hay datos sensibles
- hay estados críticos
- existe trazabilidad operativa
En esos casos, la falta de auth ya no es deuda menor. Es riesgo.
Paso 1: Define primero los roles de negocio
No empieces por JWT.
Empieza por esta pregunta:
¿quién puede hacer qué?
Ejemplo simple:
adminoperadorlector
Y luego:
admin: crea usuarios, configura, ve todooperador: crea y actualiza registros operativoslector: solo consulta
Si esto no está claro, la implementación termina siendo arbitraria.
Paso 2: Modela usuarios en la base
Una tabla mínima:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role VARCHAR(30) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Claves:
- nunca guardes contraseñas en texto plano
roledebe estar acotadois_activete permite deshabilitar sin borrar
Paso 3: Hashea contraseñas correctamente
Nunca compares passwords en claro ni inventes tu propio esquema.
Usa librerías probadas.
Ejemplo conceptual:
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
Esto no es opcional. Es la base.
Paso 4: Elige estrategia de sesión
Para apps internas, una opción común es:
- login por email/password
- emisión de token
- envío del token en cada request
Puedes usar:
- JWT
- sesiones server-side
- tokens opacos
No hay una única respuesta correcta.
Si quieres una base simple y compatible con APIs, JWT suele ser una opción práctica.
Paso 5: Crear endpoint de login
Conceptualmente:
@router.post("/api/auth/login")
def login(payload: LoginInput, db: Session = Depends(get_db)):
user = find_user_by_email(db, payload.email)
if not user or not verify_password(payload.password, user.password_hash):
raise HTTPException(status_code=401, detail="Credenciales inválidas")
if not user.is_active:
raise HTTPException(status_code=403, detail="Usuario inactivo")
token = create_access_token({
"sub": str(user.id),
"role": user.role,
})
return {"access_token": token, "token_type": "bearer"}
El objetivo no es que el login “funcione”. Es que el contrato quede claro y el acceso se controle bien.
Paso 6: Proteger endpoints
Después del login, necesitas una dependencia que valide el token y recupere el usuario actual.
Ejemplo conceptual:
def get_current_user(token: str = Depends(oauth2_scheme)):
payload = decode_token(token)
user_id = payload.get("sub")
role = payload.get("role")
...
return user
Y luego:
@router.get("/api/requests")
def list_requests(current_user = Depends(get_current_user)):
...
Sin este paso, tienes autenticación decorativa pero no protección real.
Paso 7: Añadir control por rol
Autenticación responde:
“¿Quién eres?”
Autorización responde:
“¿Qué puedes hacer?”
Ejemplo:
def require_roles(*allowed_roles):
def checker(current_user = Depends(get_current_user)):
if current_user.role not in allowed_roles:
raise HTTPException(status_code=403, detail="No autorizado")
return current_user
return checker
Uso:
@router.patch("/api/requests/{request_id}")
def update_request(
request_id: int,
current_user = Depends(require_roles("admin", "operador")),
):
...
Y para administración:
@router.post("/api/users")
def create_user(current_user = Depends(require_roles("admin"))):
...
Paso 8: No mezcles visibilidad y permisos
Un error común:
- ocultar botones en frontend
- asumir que eso resuelve autorización
No la resuelve.
El frontend puede mejorar UX, pero la seguridad real vive en backend.
Si un lector no puede editar, el endpoint debe impedirlo aunque el botón no aparezca.
Paso 9: Piensa en auditoría desde temprano
Si una acción importante cambia estado, idealmente deberías poder saber:
- quién la hizo
- cuándo la hizo
- qué cambió
No siempre hace falta una auditoría completa en la primera versión, pero sí conviene dejar el sistema listo para eso.
Campos o tablas adicionales útiles:
updated_byaudit_logs- timestamps consistentes
Paso 10: Decide bien el alcance del rol
Muchos sistemas empiezan con roles globales simples y luego descubren que necesitan algo más fino:
- acceso por área
- acceso por cliente
- acceso por recurso
No construyas eso si aún no lo necesitas.
Pero tampoco cierres la puerta a evolucionar.
Una base buena hoy:
- roles simples
- checks centralizados
- posibilidad de extender a permisos más finos mañana
Errores comunes
1) Implementar login sin autorización
Saber quién eres no significa saber qué puedes hacer.
2) Guardar contraseñas mal
No hay excusa para esto.
3) Dejar endpoints sin protección
A veces el login existe, pero varios endpoints “temporales” quedan abiertos.
4) Resolver permisos solo en frontend
Eso no es seguridad. Es presentación.
5) No pensar en usuarios inactivos
Si alguien deja la empresa o cambia de rol, necesitas poder deshabilitar acceso sin romper integridad histórica.
Qué añadir después
Una vez que esta base funciona, el siguiente nivel suele incluir:
- refresh tokens
- revocación de sesiones
- recuperación de contraseña
- MFA
- permisos más granulares
- auditoría completa
No hace falta construir todo a la vez. Pero sí conviene dejar un diseño que no te bloquee luego.
Resumen
Para una app interna seria necesitas:
- roles definidos por negocio
- usuarios persistidos correctamente
- passwords hasheados
- login y sesión/token
- endpoints protegidos
- chequeo de autorización en backend
- base lista para auditoría
La autenticación no es un accesorio que agregas al final. Es una parte estructural del sistema.
Y cuanto antes la pienses bien, menos tendrás que desmontar después.
Si necesitas diseñar auth y roles en una app interna, ordenar permisos que ya crecieron mal o montar una base backend más seria para tu operación, puedes escribirme desde Sobre Mí.