SSO
Identifica a tus usuarios de forma verificable con un JWT firmado por tu backend usando el secreto de firma de tu organización.
Concepto
El SSO de Priority Hunter resuelve dos problemas: en el portal público, restringe votar y publicar a usuarios autenticados en tu app (el portal queda en solo lectura para el resto); y en el widget, hace que la identidad del votante sea verificable criptográficamente en lugar de declarada por el navegador.
El modelo es un secreto por organización: en Studio generas un secreto de firma que solo conoce tu backend (nosotros almacenamos únicamente una copia cifrada). Tu backend firma con él un JWT HS256 con los datos del usuario, y Priority Hunter lo verifica con ese mismo secreto.
Versiones anteriores de esta guía documentaban un secreto SSO global (PH_SSO_SECRET). Ese enfoque está obsoleto: el secreto ahora es por organización y se obtiene en la página de ajustes descrita abajo. Si firmas con otro valor, la verificación fallará.
Paso 1 — Configurar en Studio
Ve a Studio › Organization Settings › SSO. Ahí encontrarás:
| Ajuste | Qué hace |
|---|---|
| Enable SSO | Activa la puerta SSO: solo los usuarios autenticados a través de tu app pueden votar o publicar en el portal público. |
| SSO Login URL | La URL de tu app a la que redirigimos a los visitantes para iniciar sesión. Le añadimos ?returnTo=<callback> para el viaje de vuelta. |
| Allowed redirect domains | Opcional, separados por comas. Restringe a qué dominios puede volver la redirección — una capa extra de seguridad. |
| Signing secret | El secreto con el que tu backend firma el token. Generate lo crea y lo muestra una sola vez — cópialo a tu gestor de secretos en ese momento. Regenerate lo rota. |
Guarda el secreto como variable de entorno de tu backend (por ejemplo PH_SIGNING_SECRET). Nunca lo incluyas en JavaScript del lado cliente — cualquiera podría firmar tokens y suplantar usuarios.
Paso 2 — Construye tu endpoint de login
El SSO Login URL que configuraste arriba es un endpoint de tu app. Su trabajo:
# Your SSO Login URL, e.g. https://app.yourcompany.com/ph-sso
GET /ph-sso?returnTo=<callback-url>
1. read the returnTo query param
2. authenticate the visitor in YOUR app
(existing session, or run your normal login flow first)
3. sign a JWT (HS256) with your Priority Hunter signing secret:
{ externalId, email, name } — expires in ~24h
4. redirect the browser to: returnTo + "&ssoToken=" + jwtreturnTo ya contiene su propia query string, por eso el token se añade con &ssoToken= (no ?). No modifiques ni recortes el valor de returnTo — devuélvelo tal cual lo recibiste.
Node.js (jose)
// Node example (jose)
import { SignJWT } from "jose";
const secret = new TextEncoder().encode(PH_SIGNING_SECRET);
const token = await new SignJWT({ externalId: user.id, email: user.email, name: user.name })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("24h")
.sign(secret);
res.redirect(returnTo + "&ssoToken=" + token);Python (PyJWT)
# Python example (PyJWT)
import time
import jwt # pip install pyjwt
token = jwt.encode(
{
"externalId": user.id,
"email": user.email,
"name": user.name,
"iat": int(time.time()),
"exp": int(time.time()) + 24 * 3600,
},
PH_SIGNING_SECRET,
algorithm="HS256",
)
return redirect(f"{return_to}&ssoToken={token}")Paso 3a — Usar el token en el widget
En el widget no hay redirección: tu página ya tiene sesión, así que genera el token en tu servidor (en el render o vía un endpoint propio) y pásalo a PH:
// Option A — pass the token at init time
PH('init', {
widgetId: 'YOUR_WIDGET_ID',
ssoToken: '<token-from-your-server>',
});
// Option B — pass it with identify (e.g. after an async fetch)
PH('identify', {
id: currentUser.id,
email: currentUser.email,
name: currentUser.name,
ssoToken: '<token-from-your-server>',
});Cuando hay ssoToken, Priority Hunter verifica la firma con el secreto de tu organización y usa la identidad del token (tiene prioridad sobre los campos planos). Sin token, los campos id/email de identify se aceptan igualmente — la petición está limitada por la lista de orígenes permitidos del widget, pero la identidad no está firmada.
Paso 3b — El portal lo hace solo
Para el portal público no tienes que integrar nada más que el endpoint de login. El flujo completo:
- El visitante pulsa «Votar» o «Iniciar sesión» en el portal y se abre el modal de SSO.
- El portal redirige a tu SSO Login URL con
?returnTo=<.../sso/callback?next=...>. - Tu endpoint autentica al usuario y firma el token.
- Rediriges de vuelta a
returnToañadiendo&ssoToken=<jwt>. - El callback del portal (
/sso/callback) verifica el token con el secreto de tu organización, crea la sesión del visitante y le devuelve a la página donde estaba (next).
Para el portal, el claim email es obligatorio — sin él no se crea la sesión. El parámetro next solo admite rutas relativas del propio portal (protección contra open redirects).
Más sobre el comportamiento del portal en Portal público.
Referencia de claims
| Claim | Tipo | Requerido | Descripción |
|---|---|---|---|
externalId | string | Sí | Tu ID de usuario interno. Es la clave principal de deduplicación de votos. |
email | string | Portal: sí · Widget: no | Email del usuario. El portal lo necesita para crear la sesión; en el widget es opcional (los usuarios solo-ID aparecen como «usuario anónimo» pero deduplican igual). |
name | string | No | Nombre visible en ideas y comentarios. |
avatarUrl | string | No | URL del avatar del usuario. |
traits | object | No | Atributos libres para segmentación (plan, rol, etc.). |
exp | number | Recomendado | Expiración estándar JWT. Recomendamos ~24 horas. |
Expiración y rotación
Firma los tokens con una expiración corta (24 horas o menos) — son credenciales de un solo uso práctico, no sesiones. Si sospechas que el secreto se ha filtrado, pulsa Regenerate en Studio: desde ese momento todos los tokens firmados con el secreto anterior dejan de verificar. Actualiza la variable de entorno de tu backend con el secreto nuevo en el mismo despliegue.
Solución de problemas
| Síntoma | Causa probable | Solución |
|---|---|---|
| El token no verifica (firma inválida) | Estás firmando con un secreto distinto al actual de la organización — p. ej. uno antiguo tras una rotación, o el secreto de otra organización/entorno. | Comprueba en Studio › Organization Settings › SSO que el secreto está «Configured» y que tu backend usa el último valor generado. |
| Funcionaba y dejó de funcionar de golpe | Alguien pulsó Regenerate — los tokens del secreto anterior quedan invalidados. | Actualiza el secreto en tu backend. |
| El portal redirige pero no crea sesión | El token no incluye el claim email, o llega expirado. | Añade email al payload y revisa el exp (y el reloj del servidor). |
| La redirección de vuelta falla o aterriza en la página equivocada | El valor de returnTo se recortó (perdió su query string) o el token se añadió con ? en vez de &. | Devuelve returnTo intacto y concatena el token con &ssoToken=. |
| El widget ignora el token | El token se pasa después de que el usuario ya interactuó, o la página no está en los orígenes permitidos del widget. | Pásalo en PH('init') o en el primer PH('identify'), y revisa la lista de orígenes del widget en Studio. |