Autenticación
Vincent Depassier · 30 de agosto de 2026
Autenticación de Usuarios Finales
Todo lo que cuelga de /{workspaceId}/auth — qué llamar, qué mandar y qué vuelve.
https://gateway.praxsuite.com/{workspaceId}/auth/...Qué necesita cada endpoint
Leé esta tabla antes que nada. La mayoría de los problemas de integración acá son una credencial equivocada, no un cuerpo equivocado.
Endpoint | Necesita |
| nada — totalmente público |
| nada — totalmente público, se abre desde un correo |
| una API key ( |
| una API key |
| una API key |
| una API key |
| una API key |
| una API key |
| una API key |
| una API key |
| el JWT del propio usuario final |
| una API key |
| una API key |
El requisito de API key es deliberado: significa que solo un llamador con una key legítima del workspace puede crear o autenticar cuentas en él. Una key pk_live_ alcanza — estos endpoints están pensados para llamarse desde el navegador.
change-password es la excepción en el otro sentido: cambia la contraseña de quien sostiene el token, así que toma el token y no una key.
Arrancar un cliente de navegador
Un frontend necesita una key publicable antes de poder llamar a nada. GET /auth/config se la entrega, sin autenticación alguna:
GET https://gateway.praxsuite.com/{workspaceId}/auth/configDevuelve la key pk_live_ del workspace. Es seguro por diseño — una key publicable es análoga a una publishable key de Stripe, y su poder viene de sus scopes, no de su secreto. Te ahorra hornearla en un build, lo que a su vez significa que rotarla no implica volver a desplegar.
Registro
POST /{workspaceId}/auth/register
Authorization: Bearer pk_live_xxxxxxxx
Content-Type: application/json
{
"email": "ana.rojas@example.com",
"password": "…",
"firstName": "Ana",
"lastName": "Rojas"
}Crea una cuenta local y manda el correo de confirmación. La cuenta puede loguearse antes de confirmar — emailVerified reporta el estado y tu app decide qué habilita.
Login
POST /{workspaceId}/auth/login
Authorization: Bearer pk_live_xxxxxxxx
Content-Type: application/json
{ "email": "ana.rojas@example.com", "password": "…" }{
"isSuccess": true,
"data": {
"accessToken": "eyJhbGciOi...",
"refreshToken": "…",
"accessTokenExpiresAt": "2026-08-30T18:40:00Z",
"refreshTokenExpiresAt": "2026-09-29T18:10:00Z",
"tokenType": "Bearer",
"user": { "id": "…", "email": "ana.rojas@example.com" }
}
}Si el workspace tiene un proveedor OIDC configurado, el login pasa por él de forma transparente — el gateway valida las credenciales contra el proveedor y devuelve su propio token. Sin redirección de navegador, sin cambios en tu cliente. Si no hay proveedor, cae en la autenticación local.
Qué hay dentro del token de acceso
El JWT es un token firmado normal, y sus claims son lo que leen los row filters y el Event Bus:
Claim | Valor |
| El id del usuario final — este es el que usan los row filters |
| Su correo |
| El id del workspace |
| Siempre |
| Una entrada por cada nombre de rol que tiene |
| Una entrada por cada id de rol que tiene |
| Presentes cuando el perfil los tiene |
| Id del token y momento de emisión |
Cualquiera de esta lista se puede referenciar desde el row filter de un rol con valueFromClaim, o desde el valor por defecto de una columna con {{claim:…}}. sub es el que vas a usar el noventa por ciento de las veces.
Refresh, y por qué importa la rotación
POST /{workspaceId}/auth/refresh
Authorization: Bearer pk_live_xxxxxxxx
{ "refreshToken": "…" }Recibís un token de acceso nuevo y un refresh token nuevo. El que mandaste queda invalidado en la misma operación.
Eso tiene una consecuencia práctica: guardá solo el refresh token más reciente. Si dos pestañas refrescan a la vez, una gana y la otra queda con un token que ya no sirve. Serializá los refresh en un solo lugar de tu cliente, o aceptá que la pestaña perdedora tenga que volver a loguearse.
El refresh token son 64 bytes aleatorios y se guarda solo como hash, junto con la IP y el user agent que lo crearon — que es lo que hace útil a la lista de sesiones del portal.
Logout
POST /{workspaceId}/auth/logout
{ "refreshToken": "…" }Revoca ese único refresh token — un dispositivo, una sesión. No toca las otras sesiones del usuario, y no invalida un token de acceso ya emitido: ese muere cuando vence.
Para terminar todas las sesiones de una, desactivá la cuenta (o usá el reset de contraseña de administrador, que también revoca sesiones).
Reset de contraseña, en tres pasos
El flujo son tres llamadas y no un enlace, deliberadamente, porque un OTP que llega por correo no debería servir mucho tiempo.
forgot-password ──▶ código de 6 dígitos por correo (válido 15 minutos)
│
▼
verify-reset-code ──▶ token de sesión corto (válido 10 minutos)
│
▼
reset-password ──▶ contraseña cambiadaPOST /auth/forgot-password { "email": "…" }
POST /auth/verify-reset-code { "email": "…", "code": "123456" }
POST /auth/reset-password { "sessionToken": "…", "newPassword": "…" }`forgot-password` siempre devuelve 200, exista o no la dirección. Lo mismo hace resend-confirmation. No es un bug a sortear: evita que el endpoint sirva para descubrir qué direcciones están registradas. Tu UI debería decir "si esa dirección existe, te mandamos un código" y decirlo en serio.
Un administrador puede en cambio enviar un enlace de reset desde la pestaña End Users, válido por 60 minutos, que le permite a la persona poner su propia contraseña. Preferí eso antes que setear la contraseña por ella.
Cambiar la contraseña estando logueado
POST /{workspaceId}/auth/change-password
Authorization: Bearer <el JWT del usuario final>
{ "currentPassword": "…", "newPassword": "…" }Es el único endpoint de auth que toma el token del usuario final en vez de una API key.
Login con un proveedor externo
Un workspace puede delegar la autenticación a cualquier proveedor OIDC — un directorio corporativo, un servidor de identidad, un login social. Cada uno se registra con un slug, un nombre visible, una URL de discovery, un client id y un secreto (cifrado en reposo, y que la API nunca devuelve).
GET /auth/oidc/{providerSlug} ──▶ { authorizationUrl }
│
│ redirigís el navegador ahí; el proveedor lo devuelve con ?code&state
▼
POST /auth/oidc/callback { code, state } ──▶ el mismo par de tokens que un login localEl resultado es idéntico a un login local: un JWT del gateway con los mismos claims. Qué proveedor usó la persona queda registrado en la cuenta como authProvider y providerSubject.
Operaciones de administración
Viven en /{workspaceId}/endusers y toman una sesión del portal, no una API key — son la API de la propia pestaña End Users.
Operación | Efecto |
Crear / actualizar / listar | Lo obvio |
Desactivar | Pone |
Eliminar (permanente) | Irreversible, se lleva los datos relacionados |
Enviar reset de contraseña | Manda un enlace de 60 minutos para que la persona la ponga |
Setear contraseña | La setea directo y revoca sus sesiones activas |
Reenviar confirmación | Invalida los tokens pendientes y manda uno nuevo |
Asignar / quitar roles | Lo único que cambia lo que puede ver |
Importar | Validar un archivo primero, después ejecutar |
Cómo se conecta todo
const GATEWAY = "https://gateway.praxsuite.com";
const WORKSPACE = "3fa85f64-5717-4562-b3fc-2c963f66afa6";
// 1. Traé la key publicable una vez, al arrancar. No necesita auth.
const { publishableKey } = await fetch(`${GATEWAY}/${WORKSPACE}/auth/config`)
.then((r) => r.json());
// 2. Logueá al usuario con ella.
async function login(email, password) {
const res = await fetch(`${GATEWAY}/${WORKSPACE}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${publishableKey}`,
},
body: JSON.stringify({ email, password }),
});
const { data } = await res.json();
// Guardá solo el refresh token más nuevo — refrescar lo rota.
localStorage.setItem("refresh", data.refreshToken);
return data.accessToken;
}
// 3. De acá en adelante, cada llamada lleva el token del usuario, no la key.
async function misPedidos(accessToken) {
return fetch(`${GATEWAY}/${WORKSPACE}/query`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
// Sin cláusula "los míos" — el row filter del rol la agrega del lado del servidor.
body: JSON.stringify({
refs: { Pedidos: "…" },
query: { from: "Pedidos", limit: 25 },
}),
}).then((r) => r.json());
}El comentario de la última llamada es el punto entero del sistema: el cliente no pide sus propias filas, y no podría pedir las de otro.
Siguiente
Roles de gateway — de dónde sale ese row filter.
Conectar una app al Event Bus — el mismo token, usado para tiempo real.