Inicio rápido para motores de reservas
Guía para copiar y pegar que añade «Iniciar sesión en Recompensas» a un motor de reservas con el flujo Authorization Code + PKCE. Los fragmentos usan TypeScript de servidor (Node), fetch y Web Crypto; adáptalos a cualquier tecnología. Es la versión práctica de la referencia de SSO de fidelización.
- Te proporcionamos un
client_idy unclient_secret(se muestra una vez) y registramos los valores exactos deredirect_urique vayas a usar. - El emisor es
https://www.guestmaker.ai/api/oidc. Lee siempre los endpoints y las JWKS desde el descubrimiento, en lugar de fijarlos en el código: rotamos las claves. - El intercambio de tokens y la verificación de id_token se ejecutan solo en tu backend. El
client_secretnunca llega al navegador.
// Load discovery once at startup and cache it.
const ISSUER = "https://www.guestmaker.ai/api/oidc";
const disco = await fetch(`${ISSUER}/.well-known/openid-configuration`).then((r) => r.json());
// disco.authorization_endpoint, disco.token_endpoint, disco.jwks_uri, ...1 · Genera PKCE, state y nonce
Crea un nuevo code_verifier por intento y su code_challenge S256. Genera valores aleatorios de state (CSRF) y nonce (protección frente a reutilización). Guarda los tres en la sesión del huésped usando como clave state.
import { randomBytes, createHash } from "node:crypto";
const base64url = (b: Buffer) =>
b.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const codeVerifier = base64url(randomBytes(32));
const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
const state = base64url(randomBytes(16));
const nonce = base64url(randomBytes(16));
// session.save({ state, nonce, codeVerifier }) // server-side, keyed by state2 · Redirige al endpoint de autorización
Solicita solo los permisos que necesitas. openid es obligatorio; añade profile email para la identidad y loyalty:read transactions:read para los datos del socio.
const authUrl = new URL(disco.authorization_endpoint);
authUrl.search = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI, // must byte-match a registered value
scope: "openid profile email loyalty:read transactions:read",
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: "S256",
}).toString();
// res.redirect(authUrl.toString()) // send the guest to our branded login3 · Gestiona el callback e intercambia el código
El huésped vuelve a tu redirect_uri con code, state y iss. Confirma que state coincide con la sesión y que iss coincide con nuestro emisor; después intercambia el código de un solo uso (unos 60 s) con client_secret_basic.
// In your /callback handler: q = request query
if (q.state !== session.state) throw new Error("state mismatch");
if (q.iss && q.iss !== ISSUER) throw new Error("issuer mismatch");
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
const tokens = await fetch(disco.token_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${basic}`,
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: q.code,
redirect_uri: REDIRECT_URI,
code_verifier: session.codeVerifier,
}),
}).then((r) => r.json());
// tokens.access_token (RS256 JWT, ~5 min), tokens.id_token4 · Verifica el id_token
Nunca confíes en un token sin verificar. Comprueba la firma frente a nuestras JWKS vinculadas a RS256, y comprueba iss, aud = your client_id, nonce y la caducidad. Usa cualquier biblioteca JOSE compatible; aquí, jose.
import { jwtVerify, createRemoteJWKSet } from "jose";
const jwks = createRemoteJWKSet(new URL(disco.jwks_uri)); // caches + refreshes on new kid
const { payload: claims } = await jwtVerify(tokens.id_token, jwks, {
algorithms: ["RS256"],
issuer: ISSUER,
audience: CLIENT_ID,
});
if (claims.nonce !== session.nonce) throw new Error("nonce mismatch");
// Identity + tier/points ride in the claims (per granted scope):
// claims.sub, claims.name, claims.email,
// claims.loyalty_tier_name, claims.points_balance, claims.member_number
// session.login({ memberId: claims.sub, accessToken: tokens.access_token })5 · Consulta datos actuales del socio
El id_token es una instantánea al iniciar sesión. Para obtener el saldo y el historial actuales, llama a los endpoints de recursos del socio con el access_token como token Bearer. Son específicos del socio: el token ya identifica al huésped, por lo que nunca pasas un correo electrónico ni un ID de socio.
const auth = { Authorization: `Bearer ${tokens.access_token}` };
const balance = await fetch(`${origin}/api/loyalty/me/balance`, { headers: auth })
.then((r) => r.json());
// { points_balance, tier, pending_balance, credit, member_number, ... }
const history = await fetch(`${origin}/api/loyalty/me/transactions?limit=20`, { headers: auth })
.then((r) => r.json());
// { transactions: [...], total, limit, offset }No hay endpoint UserInfo: el perfil, el correo y el nivel llegan como declaraciones de id_token; los datos actuales provienen de /api/loyalty/me/*. Los tokens de acceso duran unos 5 minutos. Repite el flujo al caducar: la sesión del huésped persiste entre los hoteles del grupo, por lo que suele bastar una redirección silenciosa.
- Un nuevo
code_verifierpor intento; PKCE solo conS256(nuncaplain). - Verifica
state,noncey el parámetro de respuestaiss. - Verifica la firma de id_token frente a las JWKS vinculadas a
RS256; rechazaalg: none/ HS256. redirect_uridebe coincidir exactamente, con HTTPS y sin comodines ni variantes de barra final.- Mantén
client_secreten el servidor; intercambia tokens solo desde tu backend.
Los detalles completos, el modelo de tokens y el comportamiento de revocación están en la referencia de SSO de fidelización.