Prueba de concepto de arquitectura de seguridad para APIs móviles y web.
Implementa autenticación federada, cifrado de payload de extremo a extremo y autorización de negocio.
| Animación / Presentación | Descripción |
|---|---|
| docs/index.html | Presentación ejecutiva (CTO/CSO): resumen ejecutivo, arquitectura con Keycloak, STRIDE, controles en lenguaje de negocio, resultados 17/17, roadmap · Apéndice técnico incluido |
| docs/arch.html | Architecture Review (Chief Architect): diagrama C2, ADRs con rationale, cadena de middlewares y por qué el orden importa, gestión de estado y escalabilidad, patrones de seguridad, gap analysis con esfuerzo |
| docs/request-flow.html | Flujo paso a paso: PKCE → JWT → RSA keygen → Kong → JWKS → Odoo AuthZ → JWE encrypt → decrypt (11 pasos animados) |
| docs/stride.html | Modelo de amenazas STRIDE: vectores de ataque y contramedidas implementadas por categoría |
| docs/attack-repudiation.html | Detalle de ataques repudiados: 8 categorías, controles implementados y estado de mitigación |
Abre los archivos directamente en el navegador — no requieren servidor.
Navegación en la presentación: teclas←→o barra espaciadora.
App Ionic (Web / Mobile)
│
├── Keycloak 26 ──── Autenticación PKCE + JWT RS256
│ ├── web-realm (aud: ["web-api","account"] · TTL: 5 min)
│ └── mobile-realm (aud: ["mobile-api","account"] · TTL: 5 min)
│
└── Kong 3.5 ──────── Perímetro TLS + Rate Limiting
├── /api/v1/web/** → 60 req/min · X-Channel: web
└── /api/v1/mobile/** → 30 req/min · X-Channel: mobile
│
├── api-node:3000 Identity Bridge + Odoo AuthZ (POST /api/validate-jwt-client)
├── transfers-service:3001 Transferencias (JWT + rol, sin Odoo)
└── payments-service:3002 Pagos (JWT + rol, sin Odoo)
│
└── Odoo 17 ─── Autorización de negocio (x_api_clientes)
└── PostgreSQL 15
Seguridad end-to-end:
- JWT firmado RS256, validado con JWKS de Keycloak
- Respuestas cifradas con JWE RSA-OAEP-256 + A256GCM
- Clave privada del cliente:
extractable: falseen web (efímera), persistible en mobile - Rate limiting diferenciado por canal
- Token expiry con alerta automática en la app
| Herramienta | Versión mínima | Descarga |
|---|---|---|
| Docker Desktop | 4.x | https://www.docker.com/products/docker-desktop |
| Node.js | 18 LTS o superior | https://nodejs.org |
| Python | 3.8+ | https://www.python.org |
| OpenSSL | 1.1.1+ | Incluido en Git for Windows / macOS / Linux |
| Git | cualquiera | https://git-scm.com |
Python — librería requerida para el script de pruebas:
pip3 install cryptographyWindows: usar Git Bash para ejecutar los scripts .sh.
macOS/Linux: bash nativo. Los scripts usan python3 — disponible con cualquier instalación estándar de Python 3.
macOS con Apple Silicon (M1/M2/M3): Docker Desktop for Mac incluye soporte multi-arch; las imágenes (Keycloak, Kong, Odoo) tienen builds ARM64 nativos.
git clone <URL_DEL_REPO> poc-security
cd poc-securityEjecuta el script de configuración inicial. Hace todo en orden:
bash scripts/setup.shEsto realiza automáticamente:
- Genera certificados TLS autofirmados (
shared-keys/) - Levanta todos los contenedores Docker
- Espera a que Keycloak y Odoo estén listos
- Crea los reinos, clientes, usuarios y audience mappers en Keycloak
- Inicializa la base de datos de Odoo con el addon personalizado
- Crea los registros de autorización en Odoo para el usuario de prueba
- Instala dependencias de la app Ionic
Tiempo estimado: 3–5 minutos (descarga de imágenes Docker la primera vez: ~10 min)
Si prefieres hacer el setup paso a paso o el script automático falla:
mkdir -p shared-keys
# CA autofirmada
openssl genrsa -out shared-keys/ca.key 4096
openssl req -new -x509 -days 825 -key shared-keys/ca.key \
-subj "/C=DO/O=InternalCA/OU=Security/CN=internal-ca" \
-out shared-keys/ca.pem
# Certificado del servidor (Kong)
openssl genrsa -out shared-keys/kong.key 4096
openssl req -new -key shared-keys/kong.key \
-subj "/C=DO/O=API/OU=Gateway/CN=localhost" -out /tmp/kong.csr
openssl x509 -req -in /tmp/kong.csr -CA shared-keys/ca.pem \
-CAkey shared-keys/ca.key -CAcreateserial -days 825 \
-out shared-keys/fullchain.pem
# Par RSA para JWE (key-rotator lo renueva automáticamente cada 24h)
openssl genrsa -out shared-keys/priv.pem 2048
openssl rsa -in shared-keys/priv.pem -pubout -out shared-keys/pub.pem
date > shared-keys/last_rotation.txtdocker compose up -d --buildVerifica que todos los contenedores estén corriendo:
docker ps --format "table {{.Names}}\t{{.Status}}"Deberías ver:
NAMES STATUS
kong-gateway Up X seconds (healthy)
keycloak-server Up X seconds
api-node Up X seconds
transfers-service Up X seconds
payments-service Up X seconds
odoo-server Up X seconds
db-poc Up X seconds (healthy)
key-rotator Up X seconds
certbot-service Up X seconds
Espera a que Keycloak esté listo (puede tomar 60–90 segundos):
# Esperar hasta que responda
until curl -sf http://localhost:8080/realms/master; do sleep 5; doneObtener token de admin:
ADMIN_TOKEN=$(curl -s -X POST \
"http://localhost:8080/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli&username=admin&password=admin&grant_type=password" \
| python -c "import sys,json; print(json.load(sys.stdin)['access_token'])")Crear web-realm (mobile-realm se importa automáticamente desde keycloak-config/):
curl -s -X POST "http://localhost:8080/admin/realms" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d @keycloak-config/realm-web.json
# Respuesta esperada: HTTP 201Crear usuario de prueba en mobile-realm:
curl -s -X POST "http://localhost:8080/admin/realms/mobile-realm/users" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{
"username":"testuser","email":"testuser@poc.com",
"firstName":"Test","lastName":"User",
"emailVerified":true,"enabled":true,"requiredActions":[],
"credentials":[{"type":"password","value":"Test1234!","temporary":false}]
}'
# Asignar rol user-api
USER_ID=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:8080/admin/realms/mobile-realm/users?username=testuser" \
| python -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
ROLE_ID=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:8080/admin/realms/mobile-realm/roles/user-api" \
| python -c "import sys,json; print(json.load(sys.stdin)['id'])")
curl -s -X POST \
"http://localhost:8080/admin/realms/mobile-realm/users/$USER_ID/role-mappings/realm" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d "[{\"id\":\"$ROLE_ID\",\"name\":\"user-api\"}]"
# Agregar audience mapper (mobile-api)
CLIENT_UUID=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:8080/admin/realms/mobile-realm/clients?clientId=mobile-app-client" \
| python -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
curl -s -X POST \
"http://localhost:8080/admin/realms/mobile-realm/clients/$CLIENT_UUID/protocol-mappers/models" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{
"name":"mobile-api-audience","protocol":"openid-connect",
"protocolMapper":"oidc-audience-mapper",
"config":{"included.custom.audience":"mobile-api","access.token.claim":"true","id.token.claim":"false"}
}'Crear usuario de prueba en web-realm (mismo proceso):
# Reemplaza "mobile-realm" por "web-realm", "user-api" por "user-web",
# "mobile-app-client" por "web-app-client", "mobile-api" por "web-api"
# en los comandos anteriores.# Crear la base de datos e instalar el addon
docker exec odoo-server sh -c \
"odoo --stop-after-init -d pocdb --init=api_security_poc \
--db_host=db-poc --db_port=5432 \
--db_user=admin_seguro --db_password=password_2026"
# Reiniciar Odoo para que use pocdb
docker compose restart odoo
sleep 10Crear registros de autorización en Odoo:
# Obtener los SUB de ambos usuarios
WEB_TOKEN=$(curl -s -X POST \
"http://localhost:8080/realms/web-realm/protocol/openid-connect/token" \
-d "client_id=web-app-client&username=testuser&password=Test1234!&grant_type=password" \
| python -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
WEB_SUB=$(echo $WEB_TOKEN | python -c "
import sys,base64,json
t=sys.stdin.read().strip().split('.')[1]
t+='=='*(4-len(t)%4)
print(json.loads(base64.b64decode(t))['sub'])
")
MOB_TOKEN=$(curl -s -X POST \
"http://localhost:8080/realms/mobile-realm/protocol/openid-connect/token" \
-d "client_id=mobile-app-client&username=testuser&password=Test1234!&grant_type=password" \
| python -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
MOB_SUB=$(echo $MOB_TOKEN | python -c "
import sys,base64,json
t=sys.stdin.read().strip().split('.')[1]
t+='=='*(4-len(t)%4)
print(json.loads(base64.b64decode(t))['sub'])
")
# Autenticarse en Odoo y crear registros
docker exec odoo-server sh -c "
curl -s -c /tmp/c.txt -X POST 'http://localhost:8069/web/session/authenticate' \
-H 'Content-Type: application/json' \
-d '{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"db\":\"pocdb\",\"login\":\"admin\",\"password\":\"admin\"}}' > /dev/null
curl -s -b /tmp/c.txt -X POST 'http://localhost:8069/web/dataset/call_kw' \
-H 'Content-Type: application/json' \
-d '{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"x_api_clientes\",\"method\":\"create\",\"args\":[{\"name\":\"Test User Web\",\"sub\":\"$WEB_SUB\",\"email\":\"testuser@poc.com\",\"client_id\":\"web-app-client\",\"active\":true}],\"kwargs\":{}}}' > /dev/null
curl -s -b /tmp/c.txt -X POST 'http://localhost:8069/web/dataset/call_kw' \
-H 'Content-Type: application/json' \
-d '{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"x_api_clientes\",\"method\":\"create\",\"args\":[{\"name\":\"Test User Mobile\",\"sub\":\"$MOB_SUB\",\"email\":\"testuser@poc.com\",\"client_id\":\"mobile-app-client\",\"active\":true}],\"kwargs\":{}}}' > /dev/null
"cd ionic-app/poc-security
npm installcd ionic-app/poc-security
npm start
# Abre: http://localhost:4200| Campo | Valor |
|---|---|
| Usuario | testuser |
| Contraseña | Test1234! |
| Keycloak admin | admin / admin |
| Odoo admin | admin / admin |
-
Tab "Identidad" 🛡️ → Login con
testuser / Test1234!- Muestra el canal detectado (Web/Mobile), el reino y la estrategia JWE
- Timer de expiración de sesión con alerta automática
- Botón "Solicitar datos cifrados" para probar el endpoint de identidad
-
Tab "Transferencias" 💸 → (requiere login)
- Carga la lista de transferencias recientes (JWE descifrado)
- Formulario para crear una nueva transferencia
- Muestra el token JWE cifrado recibido
-
Tab "Pagos" 💳 → (requiere login)
- Carga la lista de pagos recientes (JWE descifrado)
- Formulario para crear un nuevo pago (tarjeta / ACH)
- Muestra el token JWE cifrado recibido
Técnicas para modificar variables y estados en vivo desde las DevTools de Chrome/Edge sin tocar código.
Abre DevTools → Sources, habilita source maps (Enable JavaScript source maps), busca el archivo con Ctrl+P:
Ctrl+P → "auth.service.ts" # o crypto.service.ts, api.service.ts
Haz click en el número de línea para poner un breakpoint. Cuando el código se pause, la consola tiene acceso completo al scope:
// Con ejecución pausada en login():
this.token = "eyJhbGci..." // reemplazar token
decoded.exp = Date.now()/1000 + 9999 // extender expiración sin re-firmar
channel = 'mobile' // cambiar canal en scope localO añade debugger; temporalmente en el código donde necesites pausar.
// Obtener cualquier componente
const el = document.querySelector('app-tab1') // o 'app-root', 'ion-app'
const comp = ng.getComponent(el)
// Leer / escribir propiedades del componente
comp.isLoggedIn // leer
comp.isLoggedIn = false // escribir (fuerza estado visual)
comp.channel = 'mobile' // cambiar canal
ng.applyChanges(comp) // forzar detección de cambios en la UI
// Obtener el injector para acceder a servicios singleton
const injector = ng.getInjector(el)// Ver el JWE crudo antes de que Angular lo procese
const _fetch = window.fetch
window.fetch = async (...args) => {
const res = await _fetch(...args)
if (String(args[0]).includes('localhost:8000')) {
const clone = res.clone()
clone.text().then(body => console.log('Kong response:', args[0], '\n', body.slice(0,120)))
}
return res
}
// Simular error 403 de Odoo en transfers
window.fetch = async (...args) => {
if (String(args[0]).includes('/transfers'))
return new Response(JSON.stringify({ error: 'Cliente no autorizado por Odoo' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } })
return _fetch(...args)
}
// Restaurar fetch original
window.fetch = _fetchCon un breakpoint en crypto.service.ts dentro del método de descifrado:
// Corromper el authentication tag (últimos 5 chars) → compactDecrypt debe lanzar error
> jweToken = jweToken.slice(0, -5) + 'XXXXX'
// Truncar el JWE a 3 partes → estructura inválida
> jweToken = jweToken.split('.').slice(0, 3).join('.')
// Sustituir con JWE de otro canal → debe dar 401 en el servicio
> jweToken = tokenGuardadoDeOtroCanal// Ver qué hay en storage
Object.entries(localStorage)
Object.entries(sessionStorage)
// Inspeccionar claims del token guardado (sin verificar firma)
const raw = localStorage.getItem('access_token') // ajustar key según la app
if (raw) {
const payload = JSON.parse(atob(raw.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')))
console.table(payload)
console.log('Expira:', new Date(payload.exp * 1000))
console.log('TTL restante:', payload.exp - Date.now()/1000, 'seg')
}
// Forzar expiración inmediata para probar el timer de sesión de la UI
// (solo afecta la lectura del storage, el servidor seguirá rechazando con 401)
const parts = raw.split('.')
const p = JSON.parse(atob(parts[1].replace(/-/g,'+').replace(/_/g,'/')))
p.exp = Math.floor(Date.now()/1000) - 1 // ya expirado
// No se puede re-firmar, pero sirve para probar el comportamiento de la UI// Sobreescribir detección de Capacitor antes de que Angular arranque
// (ejecutar en la consola ANTES de que cargue la app, o en el snippet de Sources)
Object.defineProperty(window, 'Capacitor', {
get: () => ({ isNativePlatform: () => true, getPlatform: () => 'android' }),
configurable: true
})
location.reload() // recargar para que platform.service.ts lo detecte al init
// Restaurar
delete window.Capacitor
location.reload()| Objetivo | Comando |
|---|---|
| Obtener componente | ng.getComponent(document.querySelector('app-tab1')) |
| Forzar re-render | ng.applyChanges(comp) |
| Ver árbol de componentes | ng.getOwningComponent(el) |
| Obtener injector | ng.getInjector(document.querySelector('app-root')) |
| Tiempo restante del JWT | payload.exp - Date.now()/1000 |
| Interceptar fetch | window.fetch = async (...a) => { /* ... */; return originalFetch(...a) } |
| Restaurar fetch | window.fetch = _fetch (guardar referencia antes) |
Nota:
ng.getComponent()solo está disponible en builds de desarrollo (ng serve). En producción (ng build) Angular elimina estos helpers. Los monkey-patches defetchse resetean al recargar la página.
Practical steps to reduce the attack surface exposed by in-browser runtime debugging and monkey-patching.
- Build without source-maps for production — prevents easy mapping from minified code to TS sources:
# production build (no source maps)
ng build --configuration production --source-map=false-
Enable Angular production mode (already done by production build). Ensure
enableProdMode()is used inmain.tsfor production bundles. -
Remove dev-only helpers — do not expose Angular devtools globals in production (handled by production build/minification).
-
Server-side hardening (Express / Node) — enforce headers and disable dev leaks:
// in your Express app
app.disable('x-powered-by');
app.set('etag', false); // avoid leaking ciphertext size
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; object-src 'none';");-
Content Security Policy & SRI — set strict
Content-Security-Policyheaders and use Subresource Integrity for any third-party scripts to reduce XSS/monkey-patch risk. -
Protect tokens & keys in the client — avoid
localStoragefor long-lived tokens on web. Prefer secure,HttpOnlycookies or short-lived access tokens + refresh tokens stored safely on mobile (Keychain/Keystore). -
Do not trust the client for security — assume an attacker can modify runtime state (DevTools,
fetchmonkey-patches, altered inputs). Enforce every business rule and authorization check on the server (identity, role, balance, recipient allow-list, per-user limits). -
Bind client public keys server-side — when the client sends
X-Client-Public-Key, require a server-side binding step (store ephemeral pubkey per session/sub) and refuse to encrypt to arbitrary, unbound keys. -
Sign important responses — include a server-side signature (JWS) over critical fields (tx id, amount, recipient, timestamp) inside the JWE so tampering in transit is detectable.
-
Handle JWE/JWT errors safely — on decryption or verification failures return generic errors and do not leak internal details or stack traces.
-
Operational measures — enforce rate limits, anomaly detection, and log suspicious activity; keep
jose/JWT libraries up-to-date and rotate keys regularly.
These measures make runtime tampering harder and reduce the value of DevTools-based attacks, but they cannot fully prevent a malicious user who controls their browser. The authoritative controls must always be on the server.
bash scripts/test-all.shResultado esperado: 33/33 PASS
Cubre:
- Infraestructura (7 contenedores)
- Keycloak (OIDC discovery, tokens, audience claims)
- Clave RSA (simulación WebCrypto)
- Kong (security headers, correlation ID, proxy)
- 6 flujos JWE (3 servicios × 2 canales)
- 5 rechazos de seguridad (cross-channel, sin token, firma inválida)
- 4 endpoints POST (crear transferencia/pago por canal)
Valida las técnicas de debugging documentadas en la sección Debugging en runtime contra la app en vivo.
Prerequisito: la app Ionic debe estar corriendo en http://localhost:4200.
# Instalar dependencias (solo la primera vez — desde la raíz del proyecto)
npm install
# Descargar el binario de Chromium (solo la primera vez)
npx playwright install chromium
# Ejecutar
node scripts/test-runtime-debug.js
# o con el script npm:
npm run test:runtimeResultado esperado: 10/10 PASS
| # | Test | Qué valida |
|---|---|---|
| 1 | ng.getComponent() |
Angular debug API disponible en dev build |
| 2 | ng.applyChanges() |
Force change detection sin error |
| 3 | ng.getInjector() |
Acceso al DI container en runtime |
| 4 | JWT en storage | JWT en memoria del servicio (no expuesto en localStorage) |
| 5 | Monkey-patch fetch | Instalación y restauración del interceptor window.fetch |
| 6 | fetch mock 403 | Simulación de error Odoo en /transfers |
| 7 | Capacitor simulation | Override de isNativePlatform / getPlatform |
| 8 | Modificar componente | ng.getComponent('app-tab1') → Tab1Page con 13 props |
| 9 | App carga | 3 tabs visibles: Identidad / Transferencias / Pagos |
| 10 | — | (incluido en checks combinados de los anteriores) |
Los screenshots de cada paso se guardan en
docs/debug-screenshots/.
La suite usa Chromium (headless). Si el navegador no está disponible:npx playwright install chromium.
| Ruta | Servicio | Canal | Rate Limit |
|---|---|---|---|
GET /api/v1/web/api/v1/data |
api-node | web | 60/min |
GET /api/v1/mobile/api/v1/data |
api-node | mobile | 30/min |
GET /api/v1/web/transfers |
transfers-service | web | 60/min |
POST /api/v1/web/transfers |
transfers-service | web | 60/min |
GET /api/v1/mobile/transfers |
transfers-service | mobile | 30/min |
POST /api/v1/mobile/transfers |
transfers-service | mobile | 30/min |
GET /api/v1/web/payments |
payments-service | web | 60/min |
POST /api/v1/web/payments |
payments-service | web | 60/min |
GET /api/v1/mobile/payments |
payments-service | mobile | 30/min |
POST /api/v1/mobile/payments |
payments-service | mobile | 30/min |
Headers requeridos en todas las peticiones:
Authorization: Bearer <JWT>
X-Client-Public-Key: <base64( JSON(JWK) )> ← clave pública RSA como objeto JWK serializado en JSON y luego base64
Formato de respuesta: Content-Type: application/jose; charset=utf-8 (JWE compact — 5 partes separadas por .)
Headers de respuesta reales (observados en vivo):
Content-Type: application/jose; charset=utf-8
Cache-Control: no-store
X-Request-Id: <uuid> ← correlation ID (echo del plugin Kong correlation-id)
X-Kong-Request-Id: <hex> ← ID interno de Kong (diferente al anterior)
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Permissions-Policy: geolocation=()
Via: kong/3.5.0
X-Powered-ByyETagausentes — desactivados en los 3 servicios (control P4 implementado).
JWE Protected Header real (decodificado):
{ "alg": "RSA-OAEP-256", "enc": "A256GCM", "svc": "data|transfers|payments", "channel": "web|mobile" }| URL | Puerto |
|---|---|
| Keycloak | http://localhost:8080 |
| Odoo | http://localhost:8069 |
| Kong Admin API | http://localhost:8001 (solo loopback) |
poc-security/
├── api-node/ # Identity Bridge: JWT + Odoo AuthZ + JWE
├── transfers-service/ # Microservicio de transferencias
├── payments-service/ # Microservicio de pagos
├── ionic-app/
│ └── poc-security/ # App Ionic/Angular (web + mobile)
│ └── src/app/
│ ├── services/
│ │ ├── auth.service.ts # Login Keycloak + JWT expiry
│ │ ├── api.service.ts # Llamadas JWE a los servicios
│ │ ├── crypto.service.ts # WebCrypto RSA-OAEP + compactDecrypt
│ │ └── platform.service.ts # Detección web vs Capacitor
│ ├── tab1/ # Login + Identity + Timer de sesión
│ ├── tab2/ # Transferencias
│ └── tab3/ # Pagos
├── odoo-custom-addons/
│ └── api_security_poc/ # Addon Odoo: x_api_clientes + HTTP controller
├── keycloak-config/ # realm-mobile.json + realm-web.json (auto-import)
├── shared-keys/ # Certificados TLS + claves RSA (generados, no en repo)
├── scripts/
│ ├── setup.sh # Script de configuración inicial
│ ├── test-all.sh # Suite de pruebas (33 tests)
│ └── killswitch.sh # Bloqueo de emergencia vía Kong
├── docker-compose.yml
└── kong.yml # Rutas + plugins declarativos
docker logs kong-gateway --tail=30Causas comunes:
shared-keys/no existe o faltan certificados → ejecutar paso 3.1- Puerto 8000 o 8443 en uso → cambiar en
docker-compose.yml
Normal la primera vez (descarga imagen 500MB + inicializa DB).
Verifica con: docker logs keycloak-server --tail=20
- Verifica que el usuario tenga el audience mapper correcto en Keycloak
- Verifica que el INTERNAL_SECRET coincida entre api-node y Odoo
- El realm-issuer debe ser
http://localhost:8080/realms/...(no el hostname interno de Docker)
El sub del JWT no tiene registro en x_api_clientes. Repetir el paso 3.4 de Odoo.
docker logs odoo-server --tail=30
docker compose restart odoocd ionic-app/poc-security
rm -rf node_modules
npm install
npm startdocker compose down -v # Elimina contenedores Y volúmenes (borra datos)
rm -rf shared-keys/ # Elimina certificados
bash scripts/setup.sh # Volver a configurarEste es un POC con configuraciones simplificadas. Antes de llevar a producción:
| Item | POC | Producción |
|---|---|---|
| Credenciales DB | Hardcodeadas en docker-compose | Docker Secrets / Vault |
| Keycloak admin | admin/admin |
Contraseña fuerte + MFA |
secure-net |
internal: false |
internal: true |
| Certificados | Autofirmados | Let's Encrypt / CA corporativa |
| Logging | Console stdout | ELK / Loki estructurado |
| Rate limiting | Policy local (un nodo) | Redis backend (multi-nodo) |
/internal/validate-user |
Protegido con validateJWT + X-Internal-Secret |
✅ Implementado |
X-Powered-By: Express |
Desactivado en los 3 servicios: app.disable('x-powered-by') |
✅ Implementado |
| ETag en respuestas JWE | Desactivado en los 3 servicios: app.set('etag', false) |
✅ Implementado |
| Odoo AuthZ | Activo en los 3 servicios: api-node + transfers + payments | ✅ Implementado |
| Vault token | Archivo plano en disco | AppRole + TTL corto |
| Componente | Tecnología |
|---|---|
| Gateway | Kong 3.5 (OSS) |
| Identidad | Keycloak 26 |
| Backend | Node.js 20 + Express |
| Autorización | Odoo 17 (addon custom) |
| Base de datos | PostgreSQL 15 |
| App | Ionic 8 + Angular 20 + Capacitor 8 |
| Cifrado payload | JWE RSA-OAEP-256 + A256GCM (librería jose v5) |
| Auth JWT | RS256 + JWKS (librería jsonwebtoken + jwks-rsa) |