SincroDev Logo SincroDev

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:

  • admin
  • operador
  • lector

Y luego:

  • admin: crea usuarios, configura, ve todo
  • operador: crea y actualiza registros operativos
  • lector: 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
  • role debe estar acotado
  • is_active te 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_by
  • audit_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:

  1. roles definidos por negocio
  2. usuarios persistidos correctamente
  3. passwords hasheados
  4. login y sesión/token
  5. endpoints protegidos
  6. chequeo de autorización en backend
  7. 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í.