Autenticacion
Flujos de Autenticación
Este documento describe los flujos de autenticación disponibles en la plataforma.
1. Login con usuario y contraseña
Es el flujo estándar. El usuario envía email y contraseña, y recibe un JWT.
Flujo
Cliente Backend
| |
|-- POST /api/auth ------------->|
| (username, password) | valida credenciales
| | genera JWT (30 días web, 120 días app)
|<-- JWT + cookie ---------------|
Endpoint
POST /api/auth
Parámetros (form):
username: email del usuariopassword: contraseñadesktop(opcional): ID del espacios(opcional): subdominio del espacioapp_log(opcional):1si es login desde app móvil (extiende el JWT a 120 días)forcePersonal(opcional):truepara forzar login en espacio personal
Respuesta exitosa
{
"info": "logged id",
"locale": "es_AR",
"token": "<jwt>"
}
2FA
Si el usuario tiene 2FA habilitado, el login retorna un código 423 con un ID de sesión Redis. El cliente debe enviar el OTP a POST /api/auth/2fa con ese ID para completar el login.
Renovación de sesión
GET /api/ping
Valida el JWT actual y emite uno nuevo. Se usa para keep-alive y para cambiar de espacio (parámetro s).
2. SSO Google
Los usuarios pueden loguearse o crear una cuenta personal con su cuenta de Google.
Ver documentación detallada en SSO-Google.md.
Flujo resumido
Cliente Backend Google
| | |
|-- GET /api/auth/integrations/google -->| |
| (context, locale, subdomain) |-- OAuth2 authorize -------->|
| | |
| |<-- callback (code) ---------|
| | valida token |
| | busca/crea usuario |
|<-- redirect + cookie ---------| |
Signup
Si el usuario no tiene cuenta en la plataforma, se crea una personal con los datos de Google (sin contraseña).
Signin
Funciona igual que el login con contraseña. Si se hace desde un espacio, se intenta loguear en ese espacio.
3. SSO SAML
Permite a organizaciones autenticar usuarios contra su Identity Provider (IdP) corporativo usando el protocolo SAML 2.0.
Configuración
SAML se configura por espacio (workspace). La configuración se almacena en el campo saml_config de la tabla escritorios e incluye:
sso_url: URL del IdP para iniciar el loginexpected_issuer: issuer esperado en la respuesta SAMLacs_url: Assertion Consumer Service URLenabled: si SAML está habilitado
Endpoints
| Método | URL | Descripción |
|---|---|---|
| GET | /api/auth/saml/login | Inicia el flujo SAML |
| POST | /api/auth/saml/callback | Recibe la respuesta del IdP |
| GET | /api/auth/saml/status | Consulta si SAML está habilitado para un espacio |
| GET | /api/saml/metadata | Metadata XML del Service Provider |
Flujo web
Browser Backend IdP SAML
| | |
|-- GET /saml/login ------------>| |
| (workspace_id) | |
| |-- SAML AuthnRequest --------->|
| | (RelayState con workspace) |
| | |
| (usuario se autentica en el IdP) |
| | |
| |<-- POST SAMLResponse ---------|
| | valida firma + assertion |
| | busca usuario por email |
| | genera JWT |
|<-- redirect + cookie ---------| |
- El frontend redirige al usuario a
/api/auth/saml/login?workspace_id=X - El backend genera un SAML AuthnRequest y redirige al IdP
- El usuario se autentica en el IdP
- El IdP envía un POST con la SAMLResponse al callback
- El backend valida la respuesta, busca al usuario por email en el espacio, genera un JWT y redirige al frontend con la cookie seteada
Verificar estado
GET /api/auth/saml/status?workspace_id=123
{
"saml_enabled": true,
"workspace_id": 123
}
SAML para apps móviles / Capacitor (OAuth+PKCE)
El flujo SAML estándar depende de cookies y redirects entre dominios, lo cual no funciona bien en apps Capacitor (SPA corriendo en localhost dentro de un WebView). Para resolver esto, se implementó un flujo OAuth2 con PKCE que actúa como proxy delante de SAML.
¿Por qué PKCE?
- La app Capacitor corre en
http://localhost, por lo que las cookies del dominio de la API no le llegan - PKCE (Proof Key for Code Exchange) permite entregar el token de forma segura sin depender de cookies ni deep links
- El flujo SAML se reutiliza internamente sin modificaciones
Endpoints
| Método | URL | Descripción |
|---|---|---|
| GET | /api/oauth/authorize | Inicia el flujo PKCE → SAML |
| GET | /api/oauth/done | Página intermedia que entrega el authorization code |
| POST | /api/oauth/token | Intercambia code + code_verifier por JWT |
Flujo
SPA (Capacitor) Backend IdP SAML
| | |
| genera code_verifier | |
| calcula code_challenge | |
| (SHA256 + base64url) | |
| | |
|-- GET /oauth/authorize ----->| |
| workspace_id | |
| redirect_uri (localhost) | |
| code_challenge | |
| code_challenge_method=S256 | |
| state | |
| |-- SAML AuthnRequest --------->|
| | (RelayState con PKCE data) |
| | |
| (usuario se autentica en el IdP dentro del WebView) |
| | |
| |<-- POST SAMLResponse ---------|
| | valida assertion |
| | genera authorization code |
| | guarda en Redis (5 min TTL) |
| | |
|<-- redirect /oauth/done -----| |
| (page HTML con code) | |
| | |
| SPA lee code del DOM | |
| o via postMessage | |
| | |
|-- POST /oauth/token -------->| |
| code | valida code en Redis |
| code_verifier | verifica PKCE: |
| redirect_uri | SHA256(verifier)==challenge |
| | genera JWT (120 días, mobile)|
|<-- { token, locale } --------| |
Code y CodeVerifier
El code debe ser una cadena aleatoria cualquiera, con lo que hay que tener cuidado es con la generacion del code verifier. El code verifier se calcula haciendo un SHA256 pero obteniendo el resultado en base64. Este paso suele ser confuso a veces. Luego se debe quitar el = del base 64.
Ejemplo: code: my_random_code si le calculamos el SHA 256 el resultado en HEXA da: 8117c00ea8375c3b0232d5a522c711ef87861a402d61341291f56c869efda4b5 ese numero en HEXA pasado a BASE64 da: gRfADqg3XDsCMtWlIscR74eGGkAtYTQSkfVshp79pLU= y finalmente le tenemos que sacar los '=' code_verifier: gRfADqg3XDsCMtWlIscR74eGGkAtYTQSkfVshp79pLU
Ejemplo de implementación en la SPA
// 1. Generar PKCE
const codeVerifier = generateRandomString(64);
const codeChallenge = base64url(sha256(codeVerifier));
const state = generateRandomString(32);
// 2. Navegar al authorize (mismo WebView)
window.location.href =
`/api/oauth/authorize?workspace_id=${wsId}` +
`&redirect_uri=http://localhost/auth/callback` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256` +
`&state=${state}`;
// 3. Después del SAML, el WebView llega a /oauth/done
// La página hace postMessage con {code, state}
window.addEventListener('message', async (event) => {
const { code, state: returnedState } = event.data;
// Validar state para prevenir CSRF
if (returnedState !== state) return;
// 4. Intercambiar code por JWT
const resp = await fetch('/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: code,
code_verifier: codeVerifier,
redirect_uri: 'http://localhost/auth/callback'
})
});
const { token } = await resp.json();
// Guardar token para usar en requests autenticados
});
Seguridad
- Los authorization codes se guardan en Redis con TTL de 5 minutos y son de un solo uso
- Solo se acepta
code_challenge_method=S256(noplain) - El
redirect_urise valida contra localhost únicamente - El parámetro
statepreviene ataques CSRF - El flujo SAML subyacente no se modifica: la validación de firma y assertion es la misma