Servicio de autenticacion centralizado para todas las aplicaciones de Pampling. Gestiona usuarios, sesiones JWT (RS256) y registro de aplicaciones cliente. Proporciona SSO via cookies de dominio .pampl.ing.
| Campo | Valor |
|---|---|
| URL | https://login.pampl.ing |
| Repositorio | git@bitbucket.org:pampling/pampling-login.git |
| App UUID (Coolify) | p14hynlmrwbu19olodlc4nz5 |
| DB UUID (Coolify) | geo38ko9nozvmckdbdxi96qk |
| Base de datos | login_db (usuario: login_user) |
| Puerto DB local | 5433 |
| Network alias Docker | pampling-login |
Todas las apps estan en subdominios *.pampl.ing. El sistema usa cookies de dominio para SSO:
analytics.pampl.ingpampling_token validalogin.pampl.ing?redirect=https://analytics.pampl.ingpampling_token en .pampl.ing (HttpOnly, Secure, SameSite=Lax)*.pampl.inglogin.pampl.ing/.well-known/jwks.jsonEl modulo de integracion usa dos URLs distintas:
| Uso | URL | Motivo |
|---|---|---|
| Redirects al navegador | https://login.pampl.ing |
El navegador del usuario necesita la URL publica |
| Fetch JWKS (servidor) | http://pampling-login:8000 |
Dentro de Docker, los contenedores no resuelven el dominio externo |
La URL publica esta hardcodeada en el modulo. La URL interna se configura con la env var PAMPLING_LOGIN_INTERNAL.
| Token | Tipo | Duracion | Almacenamiento |
|---|---|---|---|
| Access token | JWT RS256 | 1 hora | Cookie pampling_token |
| Refresh token | Opaco (random) | 30 dias | Cookie pampling_refresh, hash SHA-256 en DB |
{
"sub": "42",
"role": "user",
"email": "juan@pampling.com",
"display_name": "Juan Perez",
"username": "juan",
"apps": ["incidencias", "vault"],
"iat": 1717000000,
"exp": 1717003600
}
El campo apps contiene los slugs de las apps a las que el usuario puede acceder. Se calcula como union de:
allowed_roles incluye el rol del usuario (acceso por rol)user_applications (acceso individual)pampling-login soporta dos mecanismos combinables para dar acceso a apps:
Cada aplicacion tiene una columna allowed_roles (CSV) con los roles a los que da acceso por defecto. Ejemplo:
| App | allowed_roles | Significado |
|---|---|---|
incidencias |
user,admin,viewer,tienda |
Cualquier usuario con esos roles entra |
dashboard |
admin |
Solo admins |
vault |
user,admin |
Users y admins |
Util cuando un grupo entero de usuarios debe tener acceso a una app (ej: "todos los tienda pueden usar incidencias").
Tabla user_applications(user_id, app_id, granted_at, granted_by) con asignaciones explicitas. Se gestiona desde el panel admin → Usuarios → Apps.
Util para conceder acceso a una app concreta a un usuario puntual sin cambiarle el rol.
Un usuario tiene acceso a una app si cualquiera de los dos mecanismos da acceso. En el panel admin, al gestionar apps de un usuario, las concedidas por rol aparecen con etiqueta "por rol" (no desmarcables) y las individuales con etiqueta "individual".
| Metodo | Path | Descripcion |
|---|---|---|
GET |
/.well-known/jwks.json |
Clave publica JWKS |
GET |
/health |
Health check |
POST |
/api/auth/register |
Registro publico (rate-limit 5/h por IP). Crea usuario con rol pendiente |
POST |
/api/auth/login |
Login (username + password) |
POST |
/api/auth/logout |
Logout (revoca refresh) |
POST |
/api/auth/refresh |
Renovar tokens |
GET |
/api/auth/verify |
Verificar token |
GET |
/api/auth/me |
Perfil del usuario |
| Metodo | Path | Descripcion |
|---|---|---|
GET/POST/PATCH/DELETE |
/api/users/* |
CRUD usuarios |
GET |
/api/users/{id}/applications |
Apps del usuario (con flag granted individual y by_role) |
POST |
/api/users/{id}/applications |
Conceder app individual (body: {app_id}) |
DELETE |
/api/users/{id}/applications/{app_id} |
Revocar app individual |
GET/POST/PATCH/DELETE |
/api/apps/* |
CRUD aplicaciones |
POST |
/api/apps/{id}/rotate-secret |
Rotar client secret |
GET |
/api/audit |
Log de auditoria (filtrable) |
Accesible en https://login.pampl.ing/admin (requiere rol admin):
register, grant_app, revoke_app)La pagina https://login.pampl.ing/register permite que cualquiera cree una cuenta. Los usuarios recien registrados:
pendiente → no tienen acceso a ninguna appuser) → heredan acceso a las apps que permiten ese rolAl hacer login, un usuario pendiente ve un mensaje "Tu cuenta esta pendiente de aprobacion".
| Tabla | Descripcion |
|---|---|
users |
Usuarios (id, email, username, password_hash, display_name, role, is_active) |
applications |
Apps registradas (id, name, slug, domain, is_active, client_secret, allowed_roles) |
user_applications |
Permisos individuales (user_id, app_id, granted_at, granted_by) |
refresh_tokens |
Tokens de refresco (user_id, token_hash, expires_at, revoked) |
audit_log |
Log de auditoria (user_id, action, app_id, ip_address, detail) |
| Rol | Descripcion |
|---|---|
admin |
Acceso completo al panel de administracion |
user |
Usuario estandar autenticado |
viewer |
Solo lectura |
tienda |
Personal de tienda - por defecto solo acceso a pampling-incidencias |
pendiente |
Usuario recien registrado sin acceso a ninguna app hasta aprobacion del admin |
Los roles especificos de cada app (ej:
requester,ai_teamen pampling-requests) se gestionan dentro de cada app, no en pampling-login.
pampling_auth.pyEste es el archivo completo que hay que copiar a backend/ de cada app. Tambien disponible en el repositorio en integration/pampling_auth.py.
Guardar como
backend/pampling_auth.pyen la app destino.
"""
Pampling Auth - Modulo de integracion para apps cliente.
Copiar este archivo a backend/ de cada app que use pampling-login.
Requiere: pyjwt[crypto]>=2.8.0
Env vars:
PAMPLING_LOGIN_INTERNAL - URL interna para JWKS (Docker: http://pampling-login:8000)
PAMPLING_APP_SLUG - Slug de esta app en pampling-login (recomendado). Si se define,
el acceso se valida contra el campo 'apps' del JWT (rol + permisos
individuales). Ej: "incidencias"
PAMPLING_ALLOWED_ROLES - [Legacy] Roles permitidos en esta app. Fallback si no hay APP_SLUG.
Ej: "user,admin,viewer"
Uso en routers FastAPI:
from backend.pampling_auth import get_current_user, require_role
from fastapi import Depends
@router.get("/api/protected")
def protected(user: dict = Depends(get_current_user)):
...
@router.get("/api/admin-only")
def admin_only(user: dict = Depends(require_role("admin"))):
...
# Proteger un router completo:
router = APIRouter(prefix="/api/datos", dependencies=[Depends(get_current_user)])
"""
import os
from fastapi import HTTPException, Request, status
from jwt import PyJWKClient, decode, ExpiredSignatureError, InvalidTokenError
# URL publica - para redirects al navegador (siempre el dominio externo)
LOGIN_URL_PUBLIC = "https://login.pampl.ing"
# URL interna - para fetch JWKS servidor-a-servidor (en Docker: http://pampling-login:8000)
LOGIN_URL_INTERNAL = os.environ.get("PAMPLING_LOGIN_INTERNAL", LOGIN_URL_PUBLIC)
JWKS_URL = f"{LOGIN_URL_INTERNAL}/.well-known/jwks.json"
# Modo recomendado: valida contra campo 'apps' del JWT (rol + permisos individuales)
APP_SLUG = os.environ.get("PAMPLING_APP_SLUG", "").strip()
# Modo legacy: valida solo rol contra lista estatica
ALLOWED_ROLES = os.environ.get("PAMPLING_ALLOWED_ROLES", "")
_jwks_client = PyJWKClient(JWKS_URL, lifespan=86400)
_allowed_roles = {r.strip() for r in ALLOWED_ROLES.split(",") if r.strip()} if ALLOWED_ROLES else None
def _extract_token(request: Request) -> str | None:
auth = request.headers.get("authorization", "")
if auth.startswith("Bearer "):
return auth[7:]
return request.cookies.get("pampling_token")
def _raise_unauth(request: Request, detail: str = "No autenticado"):
accept = request.headers.get("accept", "")
if "text/html" in accept:
redirect_url = str(request.url)
raise HTTPException(
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
headers={"Location": f"{LOGIN_URL_PUBLIC}?redirect={redirect_url}"},
)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail)
def get_current_user(request: Request) -> dict:
token = _extract_token(request)
if not token:
_raise_unauth(request)
try:
signing_key = _jwks_client.get_signing_key_from_jwt(token)
payload = decode(token, signing_key.key, algorithms=["RS256"])
except ExpiredSignatureError:
_raise_unauth(request, "Token expirado")
except InvalidTokenError:
_raise_unauth(request, "Token invalido")
user = {
"id": int(payload["sub"]),
"role": payload.get("role", "user"),
"email": payload.get("email", ""),
"display_name": payload.get("display_name", ""),
"username": payload.get("username", ""),
"apps": payload.get("apps", []),
}
# Modo recomendado: comprobar que el slug de esta app esta en el token.apps
if APP_SLUG:
if APP_SLUG not in user["apps"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No tienes acceso a esta aplicacion. Solicita acceso a un administrador.",
)
# Modo legacy: solo valida rol (para apps que aun no han migrado)
elif _allowed_roles and user["role"] not in _allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No tienes acceso a esta aplicacion",
)
return user
def require_role(*roles):
def checker(request: Request) -> dict:
user = get_current_user(request)
if user["role"] not in roles:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Sin permisos")
return user
return checker
Integrar una app existente requiere 4 pasos:
Copiar el bloque de codigo de arriba y guardarlo como backend/pampling_auth.py en la app destino.
O desde el repositorio:
cp /ruta/a/pampling-login/integration/pampling_auth.py /ruta/a/mi-app/backend/pampling_auth.py
En requirements.txt de la app:
pyjwt[crypto]>=2.8.0
En cada router que necesite autenticacion:
from backend.pampling_auth import get_current_user, require_role
from fastapi import Depends
# Ruta protegida (cualquier usuario autenticado con acceso a esta app)
@router.get("/api/datos")
def get_datos(user: dict = Depends(get_current_user)):
# user = {"id": 1, "role": "admin", "email": "...", "display_name": "...", "username": "...", "apps": [...]}
return {"data": "..."}
# Ruta con rol especifico
@router.get("/api/admin-only")
def admin_only(user: dict = Depends(require_role("admin"))):
return {"data": "..."}
Para proteger un router completo (todos sus endpoints):
from backend.pampling_auth import get_current_user
router = APIRouter(
prefix="/api/datos",
dependencies=[Depends(get_current_user)] # protege todos los endpoints
)
Recomendado (nuevo modelo con permisos individuales):
PAMPLING_LOGIN_INTERNAL=http://pampling-login:8000
PAMPLING_APP_SLUG=mi-app-slug
PAMPLING_APP_SLUG debe coincidir con el slug registrado en pampling-login para esta appapps del JWT → soporta acceso por rol Y acceso individualLegacy (solo roles, sin permisos individuales):
PAMPLING_LOGIN_INTERNAL=http://pampling-login:8000
PAMPLING_ALLOWED_ROLES=user,admin,viewer
PAMPLING_APP_SLUG tiene prioridad sobre PAMPLING_ALLOWED_ROLESMigracion: si tu app ya usa
PAMPLING_ALLOWED_ROLES, puedes migrar anadiendoPAMPLING_APP_SLUG(se activa el nuevo modelo) y opcionalmente quitarPAMPLING_ALLOWED_ROLESmas tarde.
| Tipo de request | Sin autenticacion |
|---|---|
Browser (Accept: text/html) |
Redirect 307 a login.pampl.ing?redirect=<url_actual> |
API (Accept: application/json) |
HTTP 401 JSON |
El modulo busca el JWT en este orden:
Authorization: Bearer <token>pampling_tokenRecomendacion: crear un wrapper api() reutilizable que gestione el 401 globalmente:
async function api(path, opts = {}) {
const res = await fetch(path, {
headers: { "Content-Type": "application/json", ...opts.headers },
...opts,
});
if (res.status === 401) {
window.location.href = `https://login.pampl.ing?redirect=${encodeURIComponent(window.location.origin)}`;
throw new Error("AUTH_REDIRECT");
}
if (res.status === 403) {
const data = await res.json().catch(() => ({}));
alert(data.detail || "No tienes acceso a esta aplicacion");
throw new Error("NO_ACCESS");
}
if (!res.ok) throw new Error(await res.text());
return res.json();
}
Asi no hace falta comprobar el 401/403 en cada fetch individual.
Para apps que ya tienen auth propio (ej: pampling-requests):
from backend.auth import get_current_user, require_role por from backend.pampling_auth import get_current_user, require_rolelogin.pampl.inguser es compatible: {id, role, email, display_name, username, apps}Could not translate host name "login.pampl.ing" to address: Name or service not known
Causa: la app intenta resolver login.pampl.ing dentro de Docker. Ese dominio lo gestiona Traefik desde fuera de la red Docker.
Solucion: asegurarse de que:
PAMPLING_LOGIN_INTERNAL=http://pampling-login:8000 esta definida en Coolifypampling-login (ya configurado en custom_docker_run_options)Desde cualquier contenedor en la red coolify, esta URL debe ser accesible:
http://pampling-login:8000/.well-known/jwks.json
Causa: el usuario esta autenticado pero no tiene esta app en su JWT (apps[]).
Solucion: desde el panel admin → Usuarios → boton "Apps" del usuario → marcar la app. O cambiarle el rol si esa app ya incluye ese rol en allowed_roles. El usuario necesita cerrar sesion y volver a entrar (o esperar al refresh) para que el nuevo JWT incluya la app.
Verificar que pyjwt[crypto]>=2.8.0 esta en requirements.txt. Sin la dependencia cryptography, PyJWT no puede verificar tokens RS256.
git push origin main
curl -s -X POST -H 'Authorization: Bearer <COOLIFY_TOKEN>' -d '{}' \
http://192.168.1.10:8000/api/v1/applications/p14hynlmrwbu19olodlc4nz5/restart
← Deploy · Arquitectura →