909 lines
56 KiB
HTML
909 lines
56 KiB
HTML
<!doctype html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>OAuth2 Server — Manual de uso</title>
|
|
<style>
|
|
@page {
|
|
margin: 22mm 18mm;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
|
|
color: #1f2430;
|
|
font-size: 12.5px;
|
|
line-height: 1.55;
|
|
}
|
|
.cover {
|
|
height: 245mm;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
page-break-after: always;
|
|
}
|
|
.cover .badge {
|
|
display: inline-block;
|
|
background: #4f46e5;
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
padding: 4px 12px;
|
|
border-radius: 999px;
|
|
margin-bottom: 18px;
|
|
width: fit-content;
|
|
}
|
|
.cover h1 {
|
|
font-size: 34px;
|
|
margin: 0 0 10px;
|
|
color: #111827;
|
|
}
|
|
.cover p.subtitle {
|
|
font-size: 15px;
|
|
color: #4b5563;
|
|
max-width: 420px;
|
|
margin: 0 0 28px;
|
|
}
|
|
.cover .meta {
|
|
font-size: 11.5px;
|
|
color: #9ca3af;
|
|
border-top: 1px solid #e5e7eb;
|
|
padding-top: 12px;
|
|
width: fit-content;
|
|
}
|
|
h1, h2, h3 { color: #111827; }
|
|
h2 {
|
|
font-size: 19px;
|
|
margin-top: 0;
|
|
padding-bottom: 8px;
|
|
border-bottom: 2px solid #4f46e5;
|
|
}
|
|
h3 {
|
|
font-size: 14.5px;
|
|
margin-bottom: 6px;
|
|
color: #4338ca;
|
|
}
|
|
section {
|
|
page-break-before: always;
|
|
padding-top: 4mm;
|
|
}
|
|
section:first-of-type { page-break-before: avoid; }
|
|
p { margin: 6px 0 10px; }
|
|
ul, ol { margin: 6px 0 12px; padding-left: 20px; }
|
|
li { margin-bottom: 4px; }
|
|
code {
|
|
background: #f1f0fb;
|
|
color: #4338ca;
|
|
padding: 1px 5px;
|
|
border-radius: 4px;
|
|
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
font-size: 11.5px;
|
|
}
|
|
pre {
|
|
background: #1e1e2e;
|
|
color: #e2e2f5;
|
|
padding: 12px 14px;
|
|
border-radius: 8px;
|
|
overflow-x: auto;
|
|
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
font-size: 10.8px;
|
|
line-height: 1.5;
|
|
page-break-inside: avoid;
|
|
}
|
|
pre code { background: none; color: inherit; padding: 0; }
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 10px 0 16px;
|
|
font-size: 11.5px;
|
|
page-break-inside: avoid;
|
|
}
|
|
th, td {
|
|
border: 1px solid #e5e7eb;
|
|
padding: 6px 9px;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
th {
|
|
background: #f5f5fb;
|
|
color: #374151;
|
|
font-weight: 600;
|
|
}
|
|
tr:nth-child(even) td { background: #fafafa; }
|
|
.box {
|
|
background: #f5f5fb;
|
|
border-left: 4px solid #4f46e5;
|
|
padding: 10px 14px;
|
|
border-radius: 6px;
|
|
margin: 12px 0;
|
|
font-size: 11.8px;
|
|
}
|
|
.box.warn {
|
|
background: #fffbeb;
|
|
border-left-color: #d97706;
|
|
}
|
|
.box strong { color: #111827; }
|
|
.credentials {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin: 12px 0 18px;
|
|
}
|
|
.cred-card {
|
|
flex: 1;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
padding: 12px 14px;
|
|
}
|
|
.cred-card h4 {
|
|
margin: 0 0 8px;
|
|
font-size: 12.5px;
|
|
color: #4338ca;
|
|
}
|
|
.cred-card div { margin-bottom: 3px; font-size: 11.5px; }
|
|
.cred-card .label { color: #6b7280; display: inline-block; width: 62px; }
|
|
.toc { list-style: none; padding-left: 0; }
|
|
.toc li {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
border-bottom: 1px dotted #d1d5db;
|
|
padding: 5px 0;
|
|
font-size: 13px;
|
|
}
|
|
.toc .n { color: #4f46e5; font-weight: 600; margin-right: 8px; }
|
|
.pill {
|
|
display: inline-block;
|
|
background: #eef2ff;
|
|
color: #4338ca;
|
|
border-radius: 4px;
|
|
padding: 1px 7px;
|
|
font-size: 10.5px;
|
|
font-weight: 600;
|
|
margin-right: 4px;
|
|
}
|
|
footer.pagefoot {
|
|
margin-top: 22px;
|
|
padding-top: 8px;
|
|
border-top: 1px solid #e5e7eb;
|
|
font-size: 10px;
|
|
color: #9ca3af;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="cover">
|
|
<span class="badge">Documentación técnica</span>
|
|
<h1>OAuth2 Identity Server</h1>
|
|
<p class="subtitle">Manual de uso: puesta en marcha, panel de administración, flujos OAuth2 y API REST completa para construir cualquier cliente propio (Angular, Flutter, NestJS, etc.).</p>
|
|
<div class="meta">
|
|
Laravel 12 · Passport 13 · Vue 3 + TypeScript · SCSS/BEM · spatie/laravel-permission · L5-Swagger<br>
|
|
Generado el 27 de julio de 2026 (actualizado: webhooks salientes + protección anti-SSRF)
|
|
</div>
|
|
</div>
|
|
|
|
<section>
|
|
<h2>Índice</h2>
|
|
<ul class="toc">
|
|
<li><span><span class="n">1.</span>Qué es esta app</span></li>
|
|
<li><span><span class="n">2.</span>Puesta en marcha</span></li>
|
|
<li><span><span class="n">3.</span>Usuarios por defecto (seed)</span></li>
|
|
<li><span><span class="n">4.</span>Uso de la interfaz web (panel de administración)</span></li>
|
|
<li><span><span class="n">5.</span>Verificación en dos pasos (2FA)</span></li>
|
|
<li><span><span class="n">6.</span>Analíticas: sesiones, logins y actividad</span></li>
|
|
<li><span><span class="n">7.</span>Audit log: quién cambió qué</span></li>
|
|
<li><span><span class="n">8.</span>Roles, permisos y roles por app</span></li>
|
|
<li><span><span class="n">9.</span>Idioma (español / inglés / catalán)</span></li>
|
|
<li><span><span class="n">10.</span>Cómo conectar tu app (Angular, NestJS, Flutter, etc.)</span></li>
|
|
<li><span><span class="n">11.</span>API REST completa: construí tu propio panel admin</span></li>
|
|
<li><span><span class="n">12.</span>Webhooks: enterate sin hacer polling</span></li>
|
|
<li><span><span class="n">13.</span>Documentación interactiva (Swagger)</span></li>
|
|
<li><span><span class="n">14.</span>Referencia rápida de rutas</span></li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>1. Qué es esta app</h2>
|
|
<p>
|
|
Es un <strong>servidor de identidad / OAuth2</strong> (Laravel Passport) pensado para ser el centro único de
|
|
usuarios, autenticación y permisos de un conjunto de aplicaciones propias. No es una app de negocio con datos
|
|
propios: su único "recurso" son los <strong>usuarios</strong> y sus <strong>roles</strong>.
|
|
</p>
|
|
<p>Provee tres piezas:</p>
|
|
<ul>
|
|
<li><strong>Servidor de autorización OAuth2</strong> — login, pantalla de consentimiento, y emisión de tokens
|
|
con los cuatro flujos estándar: Authorization Code + PKCE, Client Credentials, Password Grant y Personal
|
|
Access Tokens.</li>
|
|
<li><strong>Panel de administración en Vue 3 + TypeScript</strong> — la UI que trae la app "de fábrica",
|
|
servida por el propio Laravel, autenticada por sesión. Estilos en SCSS con nomenclatura BEM
|
|
(<code>resources/scss/</code>), sin frameworks de utility-classes.</li>
|
|
<li><strong>API REST completa</strong> (<code>/api/*</code>) — el <em>mismo</em> conjunto de funciones del
|
|
panel Vue (usuarios, roles por app, clientes OAuth, personal access tokens, apps autorizadas), pero
|
|
autenticada 100% por token OAuth2 (sin sesión ni cookies). Con esto se puede construir <strong>otro admin
|
|
entero</strong> en Angular, Flutter, o cualquier otro cliente — no hace falta usar la SPA Vue.</li>
|
|
</ul>
|
|
<div class="box">
|
|
<strong>Idea central:</strong> cada aplicación externa que quiera loguear usuarios se registra acá como un
|
|
<em>cliente OAuth</em> ("app"). Un mismo usuario puede tener un rol global (admin/user) y, además, un rol
|
|
distinto <em>dentro de cada app</em> (por ejemplo: admin en la app de Angular, usuario normal en la de NestJS).
|
|
</div>
|
|
<div class="box">
|
|
<strong>Dos frentes, misma API subyacente:</strong> la SPA Vue (sesión + cookies, pensada para uso interno
|
|
en el navegador de este mismo servidor) y la API REST (Bearer token, pensada para cualquier cliente externo)
|
|
exponen las <em>mismas</em> capacidades. Elegí el que corresponda: sesión para la UI que ya viene incluida,
|
|
token para construir la tuya propia.
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>2. Puesta en marcha</h2>
|
|
<h3>Requisitos</h3>
|
|
<ul>
|
|
<li>PHP 8.4+, Composer</li>
|
|
<li>Node.js + npm</li>
|
|
<li>SQLite (por defecto, cero configuración — se puede cambiar en <code>.env</code>)</li>
|
|
</ul>
|
|
<h3>Instalación (primera vez)</h3>
|
|
<pre><code>composer install
|
|
npm install
|
|
php artisan migrate --seed
|
|
npm run build</code></pre>
|
|
<div class="box">
|
|
<strong>Frontend:</strong> TypeScript (<code>npm run type-check</code> corre <code>vue-tsc</code> sin
|
|
emitir archivos) y SCSS compilado por Vite (paquete <code>sass</code>) desde
|
|
<code>resources/scss/app.scss</code> — no hay ningún framework de CSS utility-first instalado.
|
|
</div>
|
|
<h3>Levantar todo en desarrollo</h3>
|
|
<pre><code>composer run dev</code></pre>
|
|
<p>Este comando levanta en paralelo: el servidor PHP (<code>php artisan serve</code>), Vite en modo watch, la
|
|
cola de trabajos y los logs. La app queda disponible en <code>http://localhost:8000</code>.</p>
|
|
<div class="box warn">
|
|
<strong>Reiniciar todo de cero:</strong> <code>php artisan migrate:fresh --seed</code> borra la base y
|
|
vuelve a crear los usuarios/clientes de ejemplo descriptos en la sección 3. Los <code>client_secret</code>
|
|
de los clientes OAuth de ejemplo se imprimen en la consola en ese momento — solo se muestran una vez.
|
|
</div>
|
|
<div class="box">
|
|
<strong>Email:</strong> por defecto <code>MAIL_MAILER=log</code> — los emails (hoy, solo el de
|
|
recuperación de contraseña, ver sección 4) se escriben en <code>storage/logs/laravel.log</code> en vez de
|
|
enviarse de verdad, para no necesitar SMTP en desarrollo. Para producción, configurá un mailer real en
|
|
<code>.env</code> (<code>MAIL_MAILER</code>, <code>MAIL_HOST</code>, etc. — cualquiera soportado por
|
|
Laravel). El envío va encolado (<code>ShouldQueue</code>), así que también necesitás un worker corriendo
|
|
(<code>php artisan queue:work</code>, o <code>composer run dev</code> en desarrollo).
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>3. Usuarios por defecto (seed)</h2>
|
|
<p>El seeder (<code>database/seeders/DatabaseSeeder.php</code>) crea una cuenta admin y algunos usuarios de
|
|
ejemplo para poder probar la app de inmediato:</p>
|
|
<div class="credentials">
|
|
<div class="cred-card">
|
|
<h4>🛡 Administrador</h4>
|
|
<div><span class="label">Email:</span> <code>admin@example.com</code></div>
|
|
<div><span class="label">Password:</span> <code>password</code></div>
|
|
<div><span class="label">Rol global:</span> <span class="pill">admin</span></div>
|
|
<div style="margin-top:6px; color:#6b7280;">Acceso al panel de administración de usuarios.</div>
|
|
</div>
|
|
<div class="cred-card">
|
|
<h4>👤 Usuario normal</h4>
|
|
<div><span class="label">Email:</span> <code>carla@example.com</code></div>
|
|
<div><span class="label">Password:</span> <code>password</code></div>
|
|
<div><span class="label">Rol global:</span> <span class="pill">user</span></div>
|
|
<div style="margin-top:6px; color:#6b7280;">Sin acceso al panel admin. Es <strong>admin</strong> dentro de la app "Demo SPA" (rol por-app de ejemplo). Junto con <code>martin@example.com</code> y <code>bea@example.com</code>, solo están para que la sección de analíticas tenga datos de entrada.</div>
|
|
</div>
|
|
</div>
|
|
<div class="box">
|
|
El seeder también crea <code>nuevo@example.com</code> / <code>password</code> sin aprobar, para que la
|
|
pestaña <strong>"Pendientes"</strong> del panel de usuarios (sección 4) no arranque vacía.
|
|
</div>
|
|
<div class="box">
|
|
<strong>Instalación sin seed (producción):</strong> si nunca corriste <code>db:seed</code>, no hay ningún
|
|
usuario en la base. En ese caso, la <strong>primera</strong> cuenta que se registre por <code>/register</code>
|
|
queda automáticamente como <strong>admin</strong>, aprobada y con la sesión iniciada — no hace falta que
|
|
nadie más la apruebe, porque todavía no existe ningún admin que pudiera hacerlo. La pantalla de registro
|
|
avisa de esto con un mensaje cuando detecta que la base está vacía. A partir del segundo registro, vuelve
|
|
a regir el flujo normal (pendiente de aprobación por un admin).
|
|
</div>
|
|
|
|
<h3>Clientes OAuth (apps) de ejemplo</h3>
|
|
<p>El seeder también registra apps de prueba, una por cada flujo soportado. El <code>client_id</code> es fijo
|
|
por ejecución (UUID nuevo cada <code>migrate:fresh</code>); el <code>client_secret</code> solo se muestra por
|
|
consola al sembrar — si lo perdiste, hay que crear un cliente nuevo desde el panel (sección 4) o volver a
|
|
sembrar.</p>
|
|
<table>
|
|
<tr><th>Nombre</th><th>Grant type</th><th>Uso</th></tr>
|
|
<tr><td>Demo SPA (Authorization Code + PKCE)</td><td><code>authorization_code</code></td><td>Cliente público (sin secret), para apps tipo SPA/móvil.</td></tr>
|
|
<tr><td>Demo Service (Client Credentials)</td><td><code>client_credentials</code></td><td>Comunicación máquina-a-máquina, sin usuario.</td></tr>
|
|
<tr><td>Demo First-Party App (Password Grant)</td><td><code>password</code></td><td>Apps propias de confianza, login directo usuario/contraseña.</td></tr>
|
|
<tr><td>Swagger UI</td><td><code>authorization_code</code>, <code>client_credentials</code></td><td>Para probar el botón "Authorize" de la documentación interactiva.</td></tr>
|
|
<tr><td>Personal Access Client</td><td><code>personal_access</code></td><td>Interno — necesario para poder emitir Personal Access Tokens.</td></tr>
|
|
</table>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>4. Uso de la interfaz web (panel de administración)</h2>
|
|
<p>Todo se maneja desde <code>http://localhost:8000</code> (Vue Router, sin recargar página).</p>
|
|
<div class="box">
|
|
<strong>Modo oscuro:</strong> el ícono de sol/luna en la barra de navegación alterna entre claro y oscuro.
|
|
La preferencia se guarda en <code>localStorage</code> (por navegador, no por cuenta) y, si nunca la tocaste,
|
|
arranca siguiendo el modo del sistema operativo. El cambio se aplica antes del primer render — no hay
|
|
parpadeo del tema equivocado al cargar la página.
|
|
</div>
|
|
|
|
<h3>Login / Registro</h3>
|
|
<p><code>/login</code> y <code>/register</code>. El registro crea la cuenta con rol global <code>user</code>
|
|
automáticamente, pero <strong>no loguea al usuario</strong>: queda pendiente de aprobación. La pantalla muestra
|
|
un mensaje de "Cuenta creada" y el login devuelve error hasta que un admin la aprueba desde el panel de
|
|
usuarios (sección 4). <strong>Excepción:</strong> si la base de usuarios está vacía, esa primera cuenta se
|
|
crea como admin y queda logueada de una (ver sección 3).</p>
|
|
<div class="box warn">
|
|
<strong>Rate limiting:</strong> <code>POST /web-api/login</code> admite 5 intentos por minuto por
|
|
combinación de email + IP (frena fuerza bruta de contraseña sin afectar a otros usuarios que compartan
|
|
IP), y <code>POST /web-api/register</code> 5 por minuto por IP. Al superar el límite, la respuesta es
|
|
<code>429 Too Many Requests</code> con un header <code>Retry-After</code>.
|
|
</div>
|
|
|
|
<h3>Recuperación de contraseña</h3>
|
|
<p>
|
|
Desde <code>/login</code> → "¿Olvidaste tu contraseña?" (<code>/forgot-password</code>), cualquiera puede pedir
|
|
un enlace de restablecimiento con solo su email. La respuesta es <strong>siempre la misma</strong>, exista o
|
|
no una cuenta con ese email — lo contrario permitiría enumerar qué emails están registrados probando uno por
|
|
uno. Si la cuenta existe, se manda un email (ver la nota sobre <code>MAIL_MAILER</code> en la sección 2) con
|
|
un enlace a <code>/reset-password?token=...&email=...</code>, válido durante <strong>60 minutos</strong> y
|
|
de un solo uso.
|
|
</p>
|
|
<div class="box">
|
|
<strong>Al restablecer la contraseña se cierran todas las sesiones activas</strong> (los tokens OAuth
|
|
vigentes de esa cuenta, igual que "Cerrar todas" en Sesiones activas — sección 4) y queda un
|
|
<code>user.password_reset</code> en el audit log (sección 7). El motivo más común para resetear una
|
|
contraseña es sospechar que se filtró, así que no tiene sentido dejar vigente lo que se emitió con la
|
|
vieja.
|
|
</div>
|
|
<div class="box warn">
|
|
<strong>Rate limiting:</strong> tanto pedir el enlace como confirmar el restablecimiento admiten 5
|
|
intentos por minuto por combinación de email + IP.
|
|
</div>
|
|
|
|
<h3>Dashboard</h3>
|
|
<p><code>/</code> — accesos rápidos a Clientes OAuth, Personal Access Tokens, Apps autorizadas y (si sos admin)
|
|
Usuarios.</p>
|
|
|
|
<h3>Clientes OAuth</h3>
|
|
<p><code>/clients</code> — cada usuario ve y gestiona <em>sus propios</em> clientes (apps que registró). Al
|
|
crear uno se elige:</p>
|
|
<ul>
|
|
<li><strong>Authorization Code + PKCE</strong>: hay que indicar redirect URIs; opcionalmente "confidencial"
|
|
(con secret) o público (solo PKCE, sin secret).</li>
|
|
<li><strong>Client Credentials</strong>: siempre confidencial, sin redirect URI.</li>
|
|
<li><strong>Password Grant</strong>: para apps propias de confianza.</li>
|
|
</ul>
|
|
<div class="box warn">El <code>client_secret</code> se muestra <strong>una sola vez</strong>, justo después de
|
|
crear el cliente. Copiarlo en ese momento.</div>
|
|
<p>Cada cliente tiene, además, un botón <strong>"Webhooks"</strong> para registrar endpoints que reciben avisos
|
|
cuando pasa algo relevante para esa app (ver sección 12) — sin tener que hacer polling.</p>
|
|
<div class="box">
|
|
<strong>Rotar secret.</strong> Los clientes confidenciales (con <code>client_secret</code>) tienen un botón
|
|
<strong>"Rotar secret"</strong>. Genera un secret nuevo y lo muestra una sola vez, igual que al crear el
|
|
cliente — pero el secret <strong>anterior sigue siendo válido durante 48 horas</strong> (grace period), así
|
|
que podés actualizar la config de tu app sin downtime: mientras dure el grace period, ambos secrets
|
|
(viejo y nuevo) funcionan contra <code>POST /oauth/token</code>. Pasadas las 48 horas, o si rotás de
|
|
nuevo antes, el secret anterior deja de servir. No aplica a clientes públicos (Authorization Code + PKCE sin
|
|
secret) — no hay nada que rotar.
|
|
</div>
|
|
|
|
<h3>Personal Access Tokens</h3>
|
|
<p><code>/tokens</code> — tokens tipo "API key" para el propio usuario, sin pasar por el flujo de consentimiento.
|
|
Se elige nombre y scopes. El token completo también se muestra una única vez.</p>
|
|
|
|
<h3>Apps autorizadas</h3>
|
|
<p><code>/authorized-apps</code> — lista de apps de terceros a las que el usuario le dio acceso a su cuenta vía
|
|
<code>/oauth/authorize</code>, con opción de revocar el acceso en cualquier momento.</p>
|
|
|
|
<h3>Usuarios (solo admins)</h3>
|
|
<p><code>/admin/users</code> — alta, edición (nombre, email, contraseña, rol global, activo/desactivado) y baja
|
|
de cualquier usuario del sistema. Protecciones incluidas: no podés borrarte a vos mismo, no podés quitarte el
|
|
rol admin a vos mismo, no se puede borrar al último administrador. Un usuario desactivado no puede iniciar
|
|
sesión.</p>
|
|
<div class="box">
|
|
<strong>Borrado = papelera (soft delete).</strong> "Borrar" un usuario no lo elimina de la base: lo oculta
|
|
(desaparece de logins, listados y de cualquier chequeo de permisos) pero queda recuperable. El botón
|
|
<strong>"Ver papelera"</strong> arriba a la derecha del listado muestra los usuarios borrados, con dos
|
|
acciones por fila: <strong>Restaurar</strong> (vuelve a aparecer tal cual estaba) o <strong>Borrar
|
|
definitivamente</strong> (ahí sí, sin vuelta atrás).
|
|
</div>
|
|
<div class="box">
|
|
<strong>Registros pendientes de aprobación.</strong> Arriba a la derecha del listado hay tres pestañas:
|
|
<em>Ver activos</em>, <strong>Pendientes</strong> (con un contador si hay alguno) y <em>Ver papelera</em>.
|
|
Todo el que se registra por <code>/register</code> aparece en "Pendientes" hasta que un admin lo
|
|
<strong>aprueba</strong> (ahí ya puede iniciar sesión) o lo <strong>rechaza</strong> (va a la papelera,
|
|
igual que un borrado normal — se puede restaurar). Un usuario dado de alta directamente por un admin
|
|
(el formulario "Nuevo usuario" de esta misma pantalla) queda aprobado automáticamente.
|
|
</div>
|
|
<div class="box">
|
|
<strong>Sesiones activas.</strong> Dentro de "Editar" (junto a "Roles por app") hay una sección "Sesiones
|
|
activas" con los tokens OAuth vigentes de ese usuario — qué apps tienen acceso a su cuenta ahora mismo, con
|
|
scope y fecha de emisión. Cada sesión se puede revocar individualmente, o todas de una con "Cerrar todas".
|
|
Pensado para incident response: si un usuario reporta una cuenta comprometida (o algo raro aparece en
|
|
Analíticas/Audit log), un admin puede cortarle el acceso a todo sin esperar a que expire solo.
|
|
</div>
|
|
|
|
<h3>Seguridad: verificación en dos pasos</h3>
|
|
<p><code>/security</code> — cualquier usuario (no solo admins) puede activar 2FA acá. Ver sección 5.</p>
|
|
|
|
<h3>Analíticas (solo admins)</h3>
|
|
<p><code>/admin/analytics</code> — ver sección 6.</p>
|
|
|
|
<h3>Audit log (solo admins)</h3>
|
|
<p><code>/admin/audit-log</code> — ver sección 7.</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>5. Verificación en dos pasos (2FA)</h2>
|
|
<p>
|
|
Cualquier usuario (no hace falta ser admin) puede activar 2FA con TOTP desde <code>/security</code> —
|
|
compatible con Google Authenticator, Authy, 1Password, o cualquier app que hable el estándar TOTP
|
|
(RFC 6238). Es una capa extra sobre la contraseña: una vez activo, el login pide además un código de 6
|
|
dígitos.
|
|
</p>
|
|
<h3>Activación</h3>
|
|
<ol>
|
|
<li>El servidor genera un secreto (todavía no activo) y un código QR para escanear.</li>
|
|
<li>El usuario escanea el QR con su app autenticadora (o copia el secreto a mano si no puede escanear).</li>
|
|
<li>Confirma con un código real generado por la app — recién en ese momento queda activo de verdad. Un
|
|
secreto generado pero nunca confirmado no habilita nada.</li>
|
|
<li>El servidor muestra <strong>8 códigos de recuperación</strong>, una sola vez. Cada uno sirve para un
|
|
único login — son la salida de emergencia si se pierde el teléfono con la app autenticadora.</li>
|
|
</ol>
|
|
<h3>Login con 2FA activo</h3>
|
|
<p>
|
|
El login pasa a ser de dos pasos: primero email + contraseña (esto <strong>no</strong> crea sesión todavía);
|
|
si el usuario tiene 2FA activo, el servidor responde pidiendo el código en vez de loguear directo. Recién con
|
|
un código válido (TOTP o de recuperación) se crea la sesión. Un código de recuperación usado se invalida al
|
|
instante — no sirve una segunda vez.
|
|
</p>
|
|
<div class="box warn">
|
|
<strong>Rate limiting:</strong> el segundo paso del login (<code>/web-api/login/2fa</code>), confirmar la
|
|
activación (<code>/2fa/confirm</code>), desactivar (<code>/2fa/disable</code>) y regenerar códigos de
|
|
recuperación (<code>/2fa/recovery-codes</code>) admiten 6 intentos por minuto — el espacio de un código
|
|
TOTP de 6 dígitos es chico como para no limitarlo.
|
|
</div>
|
|
<div class="box">
|
|
<strong>Desactivar 2FA o regenerar códigos de recuperación:</strong> ambas acciones, desde
|
|
<code>/security</code>, piden confirmar la contraseña actual. Desactivar borra el secreto y los códigos de
|
|
recuperación por completo; para volver a activarlo hay que escanear un QR nuevo (no se reutiliza el
|
|
secreto anterior). Regenerar invalida los 8 códigos viejos y muestra 8 nuevos.
|
|
</div>
|
|
<div class="box">
|
|
<strong>Password Grant y 2FA:</strong> si el usuario tiene 2FA activo, <code>POST /oauth/token</code> con
|
|
<code>grant_type=password</code> exige además un campo <code>otp</code> con un código TOTP o de
|
|
recuperación válido, en el mismo request — sin él, o con uno inválido, el servidor responde
|
|
<code>400 invalid_grant</code> igual que con una contraseña incorrecta (no delata si el problema fue la
|
|
contraseña o el 2FA). Los usuarios sin 2FA no notan ningún cambio.
|
|
<pre><code>curl -X POST /oauth/token -d grant_type=password \
|
|
-d client_id=... -d client_secret=... \
|
|
-d username=ana@example.com -d password=... \
|
|
-d otp=123456 -d scope="users:read"</code></pre>
|
|
</div>
|
|
<p>Cada activación, desactivación y regeneración de códigos queda registrada en el audit log (sección 7).</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>6. Analíticas: sesiones, logins y actividad</h2>
|
|
<p>
|
|
Pantalla de monitoreo para el admin: cada vez que Passport emite un access token (cualquier grant type) o alguien
|
|
inicia sesión en el panel, queda un registro en <code>login_events</code>. Con eso se arman:
|
|
</p>
|
|
<ul>
|
|
<li><strong>KPIs</strong>: usuarios totales, sesiones activas (tokens OAuth vigentes, no revocados ni
|
|
expirados), logins de hoy, logins de los últimos 7 días, tasa de fallos de login y tasa de aprobación
|
|
OAuth (últimos 7 días).</li>
|
|
<li><strong>Logins por día</strong> (últimos 7/30/90 días, elegible arriba a la derecha): barras apiladas —
|
|
logins de apps (OAuth) vs. logins al panel admin (sesión) — con tooltip al pasar el mouse sobre cada día.</li>
|
|
<li><strong>Logins por app</strong>: qué apps se usan más.</li>
|
|
<li><strong>Usuarios más activos</strong>: ranking por cantidad de logins en el período.</li>
|
|
<li><strong>Funnel de autorización OAuth</strong>: por cada cliente, cuántas veces se mostró la pantalla de
|
|
consentimiento (<code>/oauth/authorize</code>), cuántas se aprobaron y cuántas se denegaron, con la tasa de
|
|
aprobación resultante.</li>
|
|
<li><strong>Tokens activos por cliente</strong>: snapshot en vivo de tokens OAuth vigentes (no revocados,
|
|
no expirados), agrupados por app.</li>
|
|
<li><strong>Uso de scopes</strong>: qué scopes (<code>users:read</code>, <code>account:write</code>, etc.)
|
|
aparecen más entre los tokens emitidos en el período.</li>
|
|
<li><strong>Actividad reciente</strong>: tabla con los últimos eventos (usuario, app, tipo, IP, cuándo).</li>
|
|
</ul>
|
|
<div class="box">
|
|
<strong>Exportar PDF:</strong> el botón "Exportar PDF" arriba a la derecha abre el diálogo de impresión del
|
|
navegador con una hoja de estilos dedicada — oculta la barra de navegación y los controles, fuerza tema
|
|
claro y expande la tabla de actividad reciente completa. No genera el PDF en el servidor: es el navegador
|
|
el que lo arma ("Guardar como PDF" en el diálogo de impresión), así que no depende de ninguna librería ni
|
|
servicio adicional.
|
|
</div>
|
|
<div class="box">
|
|
<strong>Tasa de fallos de login</strong> solo cuenta credenciales inválidas en el login de sesión (panel
|
|
admin) — no cuenta intentos contra una cuenta desactivada o pendiente de aprobación, porque eso es un
|
|
estado de cuenta, no una señal de ataque de fuerza bruta.
|
|
</div>
|
|
<div class="box">
|
|
El seeder de demo genera 30 días de actividad sintética para que estos gráficos no arranquen vacíos. En un
|
|
despliegue real, los datos son 100% eventos reales — no hay nada que "activar". El funnel de autorización y
|
|
la tasa de fallos de login sí arrancan vacíos incluso con el seed (son eventos nuevos, no incluidos en la
|
|
actividad sintética) hasta que ocurran de verdad.
|
|
</div>
|
|
<p>Misma info disponible por API: <code>GET /api/analytics/summary?days=30</code> (scope
|
|
<code>users:read</code>, requiere el permiso <code>users.view</code> — igual que el resto del panel admin).</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>7. Audit log: quién cambió qué</h2>
|
|
<p>
|
|
Pantalla de auditoría para el admin (<code>/admin/audit-log</code>): registra automáticamente los cambios que
|
|
importan desde el punto de vista de seguridad/gobernanza — no hace falta activar nada, queda un renglón cada vez
|
|
que ocurre alguno de estos eventos:
|
|
</p>
|
|
<table>
|
|
<tr><th>Acción</th><th>Cuándo se registra</th><th>Qué guarda en "Detalles"</th></tr>
|
|
<tr><td><code>user.created</code></td><td>Un admin da de alta un usuario desde el panel.</td><td>Nombre, email y rol asignado.</td></tr>
|
|
<tr><td><code>user.updated</code></td><td>Un admin edita nombre, email, rol, estado activo/inactivo o contraseña de un usuario.</td><td>Solo los campos que efectivamente cambiaron (antes → después). La contraseña nunca se guarda en texto plano, solo si cambió o no.</td></tr>
|
|
<tr><td><code>user.deleted</code></td><td>Un admin borra (soft delete) un usuario.</td><td>—</td></tr>
|
|
<tr><td><code>user.restored</code></td><td>Un admin restaura un usuario desde la papelera.</td><td>—</td></tr>
|
|
<tr><td><code>user.force_deleted</code></td><td>Un admin borra definitivamente un usuario de la papelera.</td><td>—</td></tr>
|
|
<tr><td><code>user.approved</code></td><td>Un admin aprueba un registro pendiente.</td><td>—</td></tr>
|
|
<tr><td><code>user.app_role_assigned</code></td><td>Un admin asigna un rol por-app a un usuario.</td><td>Client y rol asignado.</td></tr>
|
|
<tr><td><code>user.app_role_revoked</code></td><td>Un admin quita un rol por-app a un usuario.</td><td>Client y rol quitado.</td></tr>
|
|
<tr><td><code>client.created</code></td><td>Se registra un cliente OAuth nuevo (panel o API).</td><td>Grant type y si es confidencial.</td></tr>
|
|
<tr><td><code>client.revoked</code></td><td>Se revoca un cliente OAuth (panel o API) — cascada a sus tokens.</td><td>Cuántos tokens quedaron revocados.</td></tr>
|
|
<tr><td><code>user.2fa_enabled</code></td><td>Un usuario activa 2FA.</td><td>—</td></tr>
|
|
<tr><td><code>user.2fa_disabled</code></td><td>Un usuario desactiva 2FA.</td><td>—</td></tr>
|
|
<tr><td><code>user.2fa_recovery_regenerated</code></td><td>Un usuario regenera sus códigos de recuperación.</td><td>—</td></tr>
|
|
<tr><td><code>user.session_revoked</code></td><td>Un admin revoca un token OAuth ("sesión") de un usuario.</td><td>—</td></tr>
|
|
<tr><td><code>user.sessions_revoked_all</code></td><td>Un admin cierra todas las sesiones activas de un usuario de una vez.</td><td>Cuántos tokens quedaron revocados.</td></tr>
|
|
<tr><td><code>client.webhook_created</code></td><td>Se registra un webhook nuevo en un cliente OAuth.</td><td>URL y eventos suscritos.</td></tr>
|
|
<tr><td><code>client.webhook_deleted</code></td><td>Se borra un webhook.</td><td>URL.</td></tr>
|
|
<tr><td><code>client.secret_rotated</code></td><td>El dueño de un cliente confidencial rota su <code>client_secret</code>.</td><td>Hasta cuándo sigue valiendo el secret anterior.</td></tr>
|
|
<tr><td><code>user.password_reset</code></td><td>Un usuario restablece su contraseña vía "¿Olvidaste tu contraseña?".</td><td>Cuántas sesiones quedaron revocadas.</td></tr>
|
|
</table>
|
|
<p>Cada evento guarda además quién lo hizo (o "(sistema)" si no hay actor), IP, y cuándo. Es filtrable por
|
|
sujeto (Usuarios / Clientes OAuth) y por acción, y está paginado.</p>
|
|
<div class="box warn">
|
|
<strong>Qué NO cubre todavía:</strong> cambios de <code>name</code>/<code>redirect_uris</code> en un cliente
|
|
OAuth existente (solo se audita creación y revocación), y acciones de auto-servicio de un usuario sobre sus
|
|
propios clientes/tokens/apps autorizadas (ese flujo no pasa por rutas de admin).
|
|
</div>
|
|
<p>Requiere el permiso <code>audit.view</code> (nuevo — separado de <code>users.*</code> porque también cubre
|
|
clientes OAuth, no solo usuarios). Se lo asigna automáticamente al rol <code>admin</code>. Misma info por API:
|
|
<code>GET /api/audit-log</code> (scope <code>users:read</code>).</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>8. Roles, permisos y roles por app</h2>
|
|
<h3>Roles globales</h3>
|
|
<p>Gobiernan el panel de administración de esta app (<code>spatie/laravel-permission</code>):</p>
|
|
<table>
|
|
<tr><th>Rol</th><th>Permisos</th></tr>
|
|
<tr><td><span class="pill">admin</span></td><td><code>users.view</code>, <code>users.create</code>, <code>users.update</code>, <code>users.delete</code>, <code>audit.view</code></td></tr>
|
|
<tr><td><span class="pill">user</span></td><td>Ninguno extra — gestiona su propio perfil, sus clientes OAuth y sus tokens.</td></tr>
|
|
</table>
|
|
|
|
<h3>Roles por app</h3>
|
|
<p>Independientes del rol global: un admin le puede asignar a cualquier usuario un rol (<code>admin</code> o
|
|
<code>user</code>) <strong>específico de una app registrada</strong>, desde el panel de edición de usuario
|
|
("Roles por app"). Sirve para que, por ejemplo, un usuario sea <code>admin</code> dentro de la app Angular
|
|
pero solo <code>user</code> dentro de la app NestJS — sin tocar su rol global.</p>
|
|
<p>Esa información viaja en la respuesta de la API (<code>app_roles</code>, ver sección 11): cada app ve
|
|
únicamente el rol que ese usuario tiene <em>en ella</em>, resuelto automáticamente según el <code>client_id</code>
|
|
del token usado en la request.</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>9. Idioma (español / inglés / catalán)</h2>
|
|
<p>El selector está arriba a la derecha en la barra de navegación (<strong>ES / EN / CA</strong>). Cambiarlo
|
|
hace un POST a <code>/web-api/locale</code>, guarda el idioma en la sesión y recarga la página — el servidor
|
|
decide el idioma con el que renderiza esa carga (mensajes de validación/autenticación vía
|
|
<code>laravel-lang</code>, más los textos propios de la app en <code>lang/{es,en,ca}/messages.php</code>).</p>
|
|
<table>
|
|
<tr><th>Idioma</th><th>Mensajes del servidor</th><th>Interfaz (SPA Vue)</th></tr>
|
|
<tr><td><strong>ES</strong> (español, por defecto)</td><td>✓</td><td>✓</td></tr>
|
|
<tr><td><strong>EN</strong> (inglés)</td><td>✓</td><td>✓ — traducción completa (nav, botones, formularios, tablas, gráficos)</td></tr>
|
|
<tr><td><strong>CA</strong> (catalán)</td><td>✓</td><td>✓ — traducción completa</td></tr>
|
|
</table>
|
|
<p>La interfaz usa <code>vue-i18n</code> con diccionarios en <code>resources/js/i18n/{es,en,ca}.js</code>
|
|
(168 claves cada uno). El locale de arranque del frontend se inyecta desde el servidor
|
|
(<code>window.__LOCALE__</code> en <code>app.blade.php</code>), así que la SPA nace ya en el idioma correcto
|
|
sin parpadeo.</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>10. Cómo conectar tu app (Angular, NestJS, Flutter, etc.)</h2>
|
|
<h3>Paso 1 — Registrar la app</h3>
|
|
<p>Desde <code>/clients</code>, crear un cliente OAuth eligiendo el grant type según el tipo de app:</p>
|
|
<table>
|
|
<tr><th>Tipo de app</th><th>Grant recomendado</th></tr>
|
|
<tr><td>SPA (Angular, React) servida al navegador</td><td>Authorization Code + PKCE, cliente <strong>público</strong></td></tr>
|
|
<tr><td>Backend con secret propio (NestJS, Express)</td><td>Authorization Code, cliente <strong>confidencial</strong>, o Client Credentials si no hay usuario final</td></tr>
|
|
<tr><td>Servicio interno máquina-a-máquina</td><td>Client Credentials</td></tr>
|
|
</table>
|
|
|
|
<h3>Paso 2 — Login del usuario (Authorization Code + PKCE, ej. Angular)</h3>
|
|
<p>Redirigir el navegador a:</p>
|
|
<pre><code>GET /oauth/authorize
|
|
?client_id={client_id}
|
|
&redirect_uri={tu_callback}
|
|
&response_type=code
|
|
&scope=users:read users:write
|
|
&state={valor_random}
|
|
&code_challenge={challenge_pkce}
|
|
&code_challenge_method=S256</code></pre>
|
|
<p>El usuario loguea (o ya tiene sesión) y aprueba el acceso. Passport redirige a <code>redirect_uri</code>
|
|
con <code>?code=...&state=...</code>.</p>
|
|
|
|
<h3>Paso 3 — Canjear el código por un token</h3>
|
|
<pre><code>curl -X POST http://localhost:8000/oauth/token \
|
|
-d grant_type=authorization_code \
|
|
-d client_id={client_id} \
|
|
-d code={code} \
|
|
-d redirect_uri={tu_callback} \
|
|
-d code_verifier={verifier_pkce}</code></pre>
|
|
|
|
<h3>Alternativa — Client Credentials (ej. NestJS backend-a-backend)</h3>
|
|
<pre><code>curl -X POST http://localhost:8000/oauth/token \
|
|
-d grant_type=client_credentials \
|
|
-d client_id={client_id} \
|
|
-d client_secret={client_secret} \
|
|
-d scope="users:read"</code></pre>
|
|
<div class="box">Un token de <code>client_credentials</code> no tiene usuario asociado: actúa como
|
|
"servicio de confianza" y puede listar/crear/editar usuarios sin más chequeo que el scope del token.</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>11. API REST completa: construí tu propio panel admin</h2>
|
|
<p>
|
|
Todo lo que hace la SPA Vue por sesión también está disponible por token OAuth2. Esto significa que un cliente
|
|
Angular, Flutter, o lo que sea, puede ser un <strong>admin completo</strong> — no solo consultar usuarios —
|
|
sin depender de la sesión de Laravel. Todos los endpoints requieren <code>Authorization: Bearer {access_token}</code>.
|
|
</p>
|
|
|
|
<h3>Usuarios y roles (scopes <code>users:read</code> / <code>users:write</code>)</h3>
|
|
<table>
|
|
<tr><th>Método</th><th>Ruta</th><th>Descripción</th></tr>
|
|
<tr><td>GET</td><td><code>/api/user</code></td><td>Whoami: usuario del token actual, con <code>roles</code> y <code>app_roles</code>.</td></tr>
|
|
<tr><td>GET</td><td><code>/api/users</code></td><td>Listar usuarios (requiere <code>users.view</code>, salvo client_credentials).</td></tr>
|
|
<tr><td>POST</td><td><code>/api/users</code></td><td>Crear usuario (requiere <code>users.create</code>).</td></tr>
|
|
<tr><td>GET</td><td><code>/api/users/{id}</code></td><td>Ver un usuario. Siempre permitido sobre uno mismo.</td></tr>
|
|
<tr><td>PUT</td><td><code>/api/users/{id}</code></td><td>Editar. Uno mismo puede cambiar nombre/email/contraseña; rol/estado y otros usuarios requieren <code>users.update</code>.</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/users/{id}</code></td><td>Borrar — soft delete, va a la papelera (requiere <code>users.delete</code>; no se puede autoeliminar).</td></tr>
|
|
<tr><td>GET</td><td><code>/api/users/trashed</code></td><td>Listar usuarios en la papelera (requiere <code>users.view</code>).</td></tr>
|
|
<tr><td>POST</td><td><code>/api/users/{id}/restore</code></td><td>Restaurar un usuario borrado (requiere <code>users.update</code>).</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/users/{id}/force</code></td><td>Borrado definitivo, sin vuelta atrás (requiere <code>users.delete</code>; el usuario tiene que estar en la papelera).</td></tr>
|
|
<tr><td>GET</td><td><code>/api/users/pending</code></td><td>Listar registros pendientes de aprobación (requiere <code>users.view</code>).</td></tr>
|
|
<tr><td>POST</td><td><code>/api/users/{id}/approve</code></td><td>Aprobar un registro pendiente (requiere <code>users.update</code>).</td></tr>
|
|
<tr><td>GET</td><td><code>/api/users/{id}/app-roles</code></td><td>Roles del usuario por app (propio siempre permitido; otros requieren <code>users.view</code>).</td></tr>
|
|
<tr><td>POST</td><td><code>/api/users/{id}/app-roles</code></td><td>Asignar un rol en una app (requiere <code>users.update</code>, sin excepción para uno mismo).</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/users/{id}/app-roles/{clientId}/{role}</code></td><td>Quitar un rol en una app (requiere <code>users.update</code>).</td></tr>
|
|
<tr><td>GET</td><td><code>/api/users/{id}/sessions</code></td><td>Tokens OAuth activos de ese usuario ("sesiones") (requiere <code>users.view</code>).</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/users/{id}/sessions/{tokenId}</code></td><td>Revocar una sesión puntual (requiere <code>users.update</code>).</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/users/{id}/sessions</code></td><td>Revocar todas las sesiones activas de ese usuario (requiere <code>users.update</code>).</td></tr>
|
|
<tr><td>GET</td><td><code>/api/apps</code></td><td>Listar todas las apps registradas (requiere <code>users.view</code>) — para armar el selector de roles-por-app.</td></tr>
|
|
<tr><td>GET</td><td><code>/api/analytics/summary?days=30</code></td><td>KPIs, logins por día/app, usuarios más activos, funnel de autorización OAuth, tokens activos por cliente, uso de scopes y actividad reciente (requiere <code>users.view</code>).</td></tr>
|
|
<tr><td>GET</td><td><code>/api/audit-log?action=&subject_type=&per_page=25</code></td><td>Historial de auditoría paginado: altas/bajas de usuarios, cambios de rol, clientes OAuth creados/revocados (requiere <code>audit.view</code>).</td></tr>
|
|
</table>
|
|
|
|
<h3>Ejemplo de respuesta de usuario</h3>
|
|
<pre><code>{
|
|
"id": 2,
|
|
"name": "Carla Gómez",
|
|
"email": "carla@example.com",
|
|
"is_active": true,
|
|
"roles": ["user"],
|
|
"app_roles": ["admin"],
|
|
"created_at": "2026-07-25T08:52:39.000000Z"
|
|
}</code></pre>
|
|
<p><code>roles</code> es el rol global; <code>app_roles</code> es el rol de ese usuario específicamente en la
|
|
app dueña del token usado para la request (útil para que cada app decida qué mostrar/permitir sin tener su
|
|
propio sistema de permisos). El campo <code>deleted_at</code> solo aparece (no-null) en las respuestas de
|
|
<code>/api/users/trashed</code>.</p>
|
|
|
|
<h3>Cuenta propia: clientes, tokens y apps autorizadas (scopes <code>account:read</code> / <code>account:write</code>)</h3>
|
|
<p>Equivalente por token a <code>/web-api/clients</code>, <code>/personal-access-tokens</code> y
|
|
<code>/authorized-apps</code> de la sección 4 — siempre sobre el usuario dueño del token (no aplica a
|
|
client_credentials, que no tiene usuario asociado):</p>
|
|
<table>
|
|
<tr><th>Método</th><th>Ruta</th><th>Descripción</th></tr>
|
|
<tr><td>GET</td><td><code>/api/clients</code></td><td>Listar tus clientes OAuth (apps que registraste).</td></tr>
|
|
<tr><td>POST</td><td><code>/api/clients</code></td><td>Registrar un cliente nuevo (devuelve <code>plain_secret</code> una sola vez).</td></tr>
|
|
<tr><td>PUT</td><td><code>/api/clients/{id}</code></td><td>Editar nombre/redirect URIs.</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/clients/{id}</code></td><td>Revocar el cliente y sus tokens.</td></tr>
|
|
<tr><td>POST</td><td><code>/api/clients/{id}/rotate-secret</code></td><td>Rotar el <code>client_secret</code> (solo clientes confidenciales). Devuelve <code>plain_secret</code> una sola vez; el secret anterior sigue valiendo 48h.</td></tr>
|
|
<tr><td>GET</td><td><code>/api/personal-access-tokens</code></td><td>Listar tus Personal Access Tokens.</td></tr>
|
|
<tr><td>POST</td><td><code>/api/personal-access-tokens</code></td><td>Crear uno (el token se muestra una sola vez).</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/personal-access-tokens/{id}</code></td><td>Revocarlo.</td></tr>
|
|
<tr><td>GET</td><td><code>/api/authorized-apps</code></td><td>Apps de terceros a las que le diste acceso.</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/authorized-apps/{tokenId}</code></td><td>Revocarle el acceso a una.</td></tr>
|
|
<tr><td>GET</td><td><code>/api/clients/{id}/webhooks</code></td><td>Listar los webhooks de uno de tus clientes (ver sección 12).</td></tr>
|
|
<tr><td>POST</td><td><code>/api/clients/{id}/webhooks</code></td><td>Registrar un webhook nuevo (devuelve <code>secret</code> una sola vez).</td></tr>
|
|
<tr><td>PUT</td><td><code>/api/clients/{id}/webhooks/{webhookId}</code></td><td>Editar URL, eventos o pausar/reanudar.</td></tr>
|
|
<tr><td>DELETE</td><td><code>/api/clients/{id}/webhooks/{webhookId}</code></td><td>Borrar un webhook.</td></tr>
|
|
<tr><td>GET</td><td><code>/api/clients/{id}/webhooks/{webhookId}/deliveries</code></td><td>Historial paginado de intentos de entrega.</td></tr>
|
|
<tr><td>POST</td><td><code>/api/clients/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/retry</code></td><td>Reintentar manualmente una entrega puntual.</td></tr>
|
|
</table>
|
|
|
|
<h3>Scopes disponibles</h3>
|
|
<table>
|
|
<tr><th>Scope</th><th>Permite</th></tr>
|
|
<tr><td><code>users:read</code></td><td>Leer usuarios y roles por app</td></tr>
|
|
<tr><td><code>users:write</code></td><td>Crear, editar y borrar usuarios; asignar roles por app</td></tr>
|
|
<tr><td><code>account:read</code></td><td>Leer tus clientes OAuth, tokens y apps autorizadas</td></tr>
|
|
<tr><td><code>account:write</code></td><td>Gestionar tus clientes OAuth, tokens y apps autorizadas</td></tr>
|
|
</table>
|
|
<p>No hay scope por defecto: cada app debe pedir explícitamente los que necesita al iniciar el flujo OAuth
|
|
(ej. <code>scope=users:read users:write account:read account:write</code> para un admin completo).</p>
|
|
<div class="box warn">
|
|
<strong>Client credentials y recursos self-service:</strong> un token <code>client_credentials</code> no
|
|
tiene usuario asociado, así que <code>/api/clients</code>, <code>/api/personal-access-tokens</code> y
|
|
<code>/api/authorized-apps</code> le devuelven <code>422</code> (no hay "cuenta propia" que gestionar).
|
|
Sí puede usar <code>/api/users</code> y <code>/api/apps</code> como servicio de confianza.
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>12. Webhooks: enterate sin hacer polling</h2>
|
|
<p>
|
|
Cada cliente OAuth puede registrar uno o más endpoints webhook desde <code>/clients</code> → botón
|
|
"Webhooks" (o por API, sección 11). Cuando pasa algo relevante para ese cliente, este servidor le manda un
|
|
<code>POST</code> con el evento — no hace falta consultar la API cada tanto para enterarse.
|
|
</p>
|
|
<h3>Eventos disponibles</h3>
|
|
<table>
|
|
<tr><th>Evento</th><th>Cuándo se dispara</th><th>A quién le llega</th></tr>
|
|
<tr><td><code>token.revoked</code></td><td>Se revoca un token de ese cliente — por el usuario dueño (self-service), por un admin (Sesiones activas, sección 4), o en cascada al revocar el cliente entero.</td><td>Los webhooks de <strong>ese</strong> cliente.</td></tr>
|
|
<tr><td><code>user.deactivated</code></td><td>Un admin desactiva un usuario.</td><td>Los webhooks de todo cliente que tenga al menos un token vigente para ese usuario en ese momento.</td></tr>
|
|
</table>
|
|
<p>Con <code>token.revoked</code> tu app se entera al instante de que ya no tiene acceso a la cuenta de ese
|
|
usuario (por ejemplo, para cerrarle la sesión local), en vez de esperar a que el token expire solo.</p>
|
|
|
|
<h3>URLs permitidas</h3>
|
|
<div class="box warn">
|
|
<strong>Protección anti-SSRF:</strong> la URL de un webhook tiene que resolver a una dirección pública.
|
|
Se rechazan <code>localhost</code>, IPs loopback (<code>127.0.0.0/8</code>), privadas
|
|
(<code>10.0.0.0/8</code>, <code>172.16.0.0/12</code>, <code>192.168.0.0/16</code>) y de link-local
|
|
(<code>169.254.0.0/16</code> — incluye el endpoint de metadata de instancia en AWS/GCP/Azure,
|
|
<code>169.254.169.254</code>). También esquemas que no sean <code>http</code>/<code>https</code>.
|
|
Sin esto, cualquiera que registre un client podría hacer que este servidor le pegue a servicios internos
|
|
(Redis, la propia API, metadata de la nube) y leer la respuesta en el panel — el clásico ataque SSRF.
|
|
La validación se repite justo antes de cada envío, no solo al crear el webhook: un dominio público hoy
|
|
podría resolver a una IP interna mañana (DNS rebinding).
|
|
</div>
|
|
|
|
<h3>Verificar la firma</h3>
|
|
<p>
|
|
Cada entrega incluye el header <code>X-Webhook-Signature: sha256={firma}</code>, un HMAC-SHA256 del body crudo
|
|
(JSON) usando el <code>secret</code> que se te mostró al crear el webhook (una sola vez, igual que un
|
|
<code>client_secret</code> — si lo perdiste, borrá el webhook y creá uno nuevo). Verificalo antes de confiar en
|
|
el payload:
|
|
</p>
|
|
<pre><code>// Node.js
|
|
import crypto from 'crypto';
|
|
|
|
function isValid(rawBody: string, signatureHeader: string, secret: string): boolean {
|
|
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
|
|
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
|
|
}</code></pre>
|
|
<p>El body tiene forma <code>{"event": "token.revoked", "data": {...}}</code>. El header
|
|
<code>X-Webhook-Event</code> repite el nombre del evento, por si preferís ramificar por header sin parsear el body.</p>
|
|
|
|
<h3>Reintentos</h3>
|
|
<div class="box">
|
|
Si tu endpoint no responde <code>2xx</code> (o no responde en 5 segundos), se reintenta hasta 2 veces más
|
|
con backoff creciente (10s, 60s, 300s). Después de 3 intentos fallidos, se abandona esa entrega — no hay
|
|
cola de reintento infinita. Cada intento (éxito o fracaso) queda registrado; el panel muestra el estado del
|
|
último envío junto a cada webhook (código de respuesta o "pendiente").
|
|
</div>
|
|
<div class="box warn">
|
|
<strong>Pausar en vez de borrar:</strong> si tu endpoint va a estar caído un rato (deploy, mantenimiento),
|
|
pausalo desde el panel en vez de dejar que fallen los reintentos — un webhook pausado no genera intentos de
|
|
entrega hasta que lo reactivés.
|
|
</div>
|
|
|
|
<h3>Historial de entregas y reintento manual</h3>
|
|
<p>
|
|
El botón <strong>"Ver historial"</strong> de cada webhook (junto a Pausar/Borrar) abre el listado paginado de
|
|
todas las entregas — no solo la última. Cada fila muestra el código de respuesta (o "pendiente" si nunca
|
|
respondió), el evento, cuándo se creó y cuántos intentos acumula. Las entregas que no llegaron a un
|
|
<code>2xx</code> tienen un botón <strong>"Reintentar"</strong>: vuelve a encolar el mismo registro (no crea
|
|
uno nuevo — el contador de intentos sigue acumulando sobre esa entrega) para forzar un envío inmediato sin
|
|
esperar al backoff automático. Útil después de arreglar algo en tu endpoint y no querer esperar los 5 minutos
|
|
del backoff.
|
|
</p>
|
|
<p>Cada alta y baja de un webhook queda en el audit log (sección 7): <code>client.webhook_created</code> /
|
|
<code>client.webhook_deleted</code>.</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>13. Documentación interactiva (Swagger)</h2>
|
|
<p>Disponible en <code>http://localhost:8000/api/documentation</code>. Incluye el botón <strong>Authorize</strong>
|
|
para probar los tres flujos (Authorization Code, Client Credentials, Password) directamente desde el navegador,
|
|
usando el cliente de ejemplo <em>"Swagger UI"</em> sembrado (sección 3).</p>
|
|
<p>El JSON crudo de la especificación OpenAPI está en <code>/docs</code>.</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>14. Referencia rápida de rutas</h2>
|
|
<h3>Interfaz web (sesión, uso interno de esta app)</h3>
|
|
<table>
|
|
<tr><th>Ruta</th><th>Qué hace</th></tr>
|
|
<tr><td><code>POST /web-api/register</code></td><td>Registro — crea la cuenta pendiente, no loguea.</td></tr>
|
|
<tr><td><code>POST /web-api/login</code> / <code>/logout</code></td><td>Autenticación por sesión. Si el usuario tiene 2FA, <code>/login</code> devuelve <code>{"two_factor":true}</code> en vez de loguear.</td></tr>
|
|
<tr><td><code>POST /web-api/login/2fa</code></td><td>Segundo paso del login con 2FA activo (código TOTP o de recuperación).</td></tr>
|
|
<tr><td><code>POST /web-api/forgot-password</code></td><td>Pedir el enlace de restablecimiento (respuesta genérica, exista o no la cuenta).</td></tr>
|
|
<tr><td><code>POST /web-api/reset-password</code></td><td>Confirmar el restablecimiento con el token del email — revoca todas las sesiones activas.</td></tr>
|
|
<tr><td><code>GET /web-api/me</code></td><td>Usuario logueado (con roles/permisos y <code>two_factor_enabled</code>).</td></tr>
|
|
<tr><td><code>POST /web-api/2fa/setup</code></td><td>Genera secreto pendiente + QR para activar 2FA.</td></tr>
|
|
<tr><td><code>POST /web-api/2fa/confirm</code></td><td>Confirma la activación con un código real — devuelve los códigos de recuperación una vez.</td></tr>
|
|
<tr><td><code>POST /web-api/2fa/disable</code></td><td>Desactiva 2FA (requiere contraseña actual).</td></tr>
|
|
<tr><td><code>POST /web-api/2fa/recovery-codes</code></td><td>Regenera los códigos de recuperación (requiere contraseña actual).</td></tr>
|
|
<tr><td><code>GET/POST/PUT/DELETE /web-api/clients</code></td><td>Clientes OAuth propios.</td></tr>
|
|
<tr><td><code>POST /web-api/clients/{id}/rotate-secret</code></td><td>Rotar el <code>client_secret</code> de un cliente confidencial propio (secret anterior válido 48h más).</td></tr>
|
|
<tr><td><code>GET/POST/DELETE /web-api/personal-access-tokens</code></td><td>Personal Access Tokens propios.</td></tr>
|
|
<tr><td><code>GET/DELETE /web-api/authorized-apps</code></td><td>Apps de terceros autorizadas.</td></tr>
|
|
<tr><td><code>GET/POST/PUT/DELETE /web-api/clients/{id}/webhooks</code></td><td>Webhooks de un cliente propio.</td></tr>
|
|
<tr><td><code>GET /web-api/clients/{id}/webhooks/{webhookId}/deliveries</code></td><td>Historial paginado de entregas de un webhook propio.</td></tr>
|
|
<tr><td><code>POST /web-api/clients/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/retry</code></td><td>Reintentar manualmente una entrega.</td></tr>
|
|
<tr><td><code>GET/POST/PUT/DELETE /web-api/admin/users</code></td><td>Administración de usuarios (admin).</td></tr>
|
|
<tr><td><code>GET /web-api/admin/users/trashed</code></td><td>Usuarios en la papelera (admin).</td></tr>
|
|
<tr><td><code>POST /web-api/admin/users/{id}/restore</code></td><td>Restaurar un usuario borrado (admin).</td></tr>
|
|
<tr><td><code>DELETE /web-api/admin/users/{id}/force</code></td><td>Borrado definitivo (admin).</td></tr>
|
|
<tr><td><code>GET /web-api/admin/users/pending</code></td><td>Registros pendientes de aprobación (admin).</td></tr>
|
|
<tr><td><code>POST /web-api/admin/users/{id}/approve</code></td><td>Aprobar un registro pendiente (admin).</td></tr>
|
|
<tr><td><code>GET /web-api/admin/apps</code></td><td>Listado de todas las apps registradas (admin).</td></tr>
|
|
<tr><td><code>GET/POST/DELETE /web-api/admin/users/{id}/app-roles</code></td><td>Roles por app de un usuario (admin).</td></tr>
|
|
<tr><td><code>GET /web-api/admin/users/{id}/sessions</code></td><td>Tokens OAuth activos de un usuario (admin).</td></tr>
|
|
<tr><td><code>DELETE /web-api/admin/users/{id}/sessions/{tokenId}</code></td><td>Revocar una sesión puntual (admin).</td></tr>
|
|
<tr><td><code>DELETE /web-api/admin/users/{id}/sessions</code></td><td>Revocar todas las sesiones activas de un usuario (admin).</td></tr>
|
|
<tr><td><code>GET /web-api/admin/analytics/summary</code></td><td>Sesiones, logins, funnel de autorización, tokens activos, uso de scopes y actividad (admin).</td></tr>
|
|
<tr><td><code>GET /web-api/admin/audit-log</code></td><td>Historial de auditoría paginado y filtrable (admin).</td></tr>
|
|
<tr><td><code>POST /web-api/oauth/authorize-events</code></td><td>Registro interno del funnel de autorización (llamado por la pantalla de consentimiento, no pensado para uso directo).</td></tr>
|
|
<tr><td><code>POST /web-api/locale</code></td><td>Cambiar idioma (es/en/ca).</td></tr>
|
|
</table>
|
|
|
|
<h3>OAuth2 (Passport)</h3>
|
|
<table>
|
|
<tr><th>Ruta</th><th>Qué hace</th></tr>
|
|
<tr><td><code>GET /oauth/authorize</code></td><td>Inicio del flujo Authorization Code (pantalla de consentimiento).</td></tr>
|
|
<tr><td><code>POST /oauth/token</code></td><td>Emisión/renovación de tokens (todos los grant types).</td></tr>
|
|
</table>
|
|
<div class="box warn">
|
|
<strong>Rate limiting:</strong> todas las rutas <code>/oauth/*</code> admiten 20 requests por minuto por
|
|
combinación de IP + <code>client_id</code> — protege especialmente al Password Grant (usuario/contraseña
|
|
directos contra <code>/oauth/token</code>) de fuerza bruta, sin penalizar a otros clientes si uno solo
|
|
está siendo atacado. Va además del <code>throttle</code> genérico (60/min por IP) que Passport ya trae
|
|
por defecto en <code>/oauth/token</code>.
|
|
</div>
|
|
|
|
<h3>API REST (Bearer token, para apps externas — Angular, Flutter, lo que sea)</h3>
|
|
<table>
|
|
<tr><th>Ruta</th><th>Qué hace</th></tr>
|
|
<tr><td><code>GET /api/user</code></td><td>Whoami.</td></tr>
|
|
<tr><td><code>GET/POST/PUT/DELETE /api/users</code></td><td>Gestión de usuarios (DELETE = papelera).</td></tr>
|
|
<tr><td><code>GET /api/users/trashed</code></td><td>Usuarios en la papelera.</td></tr>
|
|
<tr><td><code>POST /api/users/{id}/restore</code></td><td>Restaurar un usuario borrado.</td></tr>
|
|
<tr><td><code>DELETE /api/users/{id}/force</code></td><td>Borrado definitivo.</td></tr>
|
|
<tr><td><code>GET /api/users/pending</code></td><td>Registros pendientes de aprobación.</td></tr>
|
|
<tr><td><code>POST /api/users/{id}/approve</code></td><td>Aprobar un registro pendiente.</td></tr>
|
|
<tr><td><code>GET/POST/DELETE /api/users/{id}/app-roles</code></td><td>Roles por app de un usuario.</td></tr>
|
|
<tr><td><code>GET/DELETE /api/users/{id}/sessions</code></td><td>Tokens OAuth activos de un usuario; revocar uno o todos.</td></tr>
|
|
<tr><td><code>GET /api/apps</code></td><td>Listado de apps registradas.</td></tr>
|
|
<tr><td><code>GET /api/analytics/summary</code></td><td>Sesiones, logins, funnel de autorización, tokens activos, uso de scopes y actividad.</td></tr>
|
|
<tr><td><code>GET /api/audit-log</code></td><td>Historial de auditoría paginado y filtrable.</td></tr>
|
|
<tr><td><code>GET/POST/PUT/DELETE /api/clients</code></td><td>Tus clientes OAuth.</td></tr>
|
|
<tr><td><code>POST /api/clients/{id}/rotate-secret</code></td><td>Rotar el <code>client_secret</code> de un cliente confidencial (secret anterior válido 48h más).</td></tr>
|
|
<tr><td><code>GET/POST/DELETE /api/personal-access-tokens</code></td><td>Tus Personal Access Tokens.</td></tr>
|
|
<tr><td><code>GET/DELETE /api/authorized-apps</code></td><td>Apps de terceros que autorizaste.</td></tr>
|
|
<tr><td><code>GET/POST/PUT/DELETE /api/clients/{id}/webhooks</code></td><td>Webhooks de uno de tus clientes.</td></tr>
|
|
<tr><td><code>GET /api/clients/{id}/webhooks/{webhookId}/deliveries</code></td><td>Historial paginado de entregas de un webhook.</td></tr>
|
|
<tr><td><code>POST /api/clients/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/retry</code></td><td>Reintentar manualmente una entrega.</td></tr>
|
|
</table>
|
|
|
|
<footer class="pagefoot">OAuth2 Identity Server — Manual de uso · generado automáticamente a partir del estado actual del proyecto.</footer>
|
|
</section>
|
|
|
|
</body>
|
|
</html>
|